diff --git a/docs/docs/connection-reference.md b/docs/docs/connection-reference.md index 7cf0670..b38da52 100644 --- a/docs/docs/connection-reference.md +++ b/docs/docs/connection-reference.md @@ -68,6 +68,25 @@ node dist/src/index.js --postgresql --host localhost --database sample_db --user node dist/src/index.js --postgresql --host dbserver.example.com --database sample_db --user appuser --password Secure123! --port 5433 --ssl true ``` +## MySQL Connection Options + +| Option | Description | Default | Required | +|--------|-------------|---------|----------| +| `--mysql` | Specifies MySQL mode | - | Yes | +| `--host` | MySQL hostname or IP | - | Yes | +| `--database` | Database name | - | Yes | +| `--user` | MySQL username | - | No | +| `--password` | MySQL password | - | No | +| `--port` | MySQL port | 3306 | No | +| `--ssl` | Use SSL connection (true/false or object) | false | No | +| `--connection-timeout` | Connection timeout in ms | 30000 | No | + +### Example + +```bash +node dist/src/index.js --mysql --host localhost --database sample_db --port 3306 --user root --password secret +``` + ## Environment Variables Instead of specifying sensitive credentials on the command line, you can use environment variables: diff --git a/docs/docs/release-notes.md b/docs/docs/release-notes.md index 1140b9b..4deceb6 100644 --- a/docs/docs/release-notes.md +++ b/docs/docs/release-notes.md @@ -53,4 +53,12 @@ - Table management (CREATE, ALTER, DROP) - Schema introspection - MCP integration for Claude Desktop -- Node.js-based implementation for cross-platform support \ No newline at end of file +- Node.js-based implementation for cross-platform support + +## 1.1.0 (2024-05-30) + +### Features +- Added MySQL database support (read/write/query, schema, etc.) +- Support for passing MySQL port via CLI and config +- Improved port validation and debug logging for MySQL +- Updated documentation and examples for MySQL and port usage \ No newline at end of file diff --git a/index.ts b/index.ts index 1b130f8..8fe5805 100644 --- a/index.ts +++ b/index.ts @@ -14,7 +14,7 @@ import sqlite3 from "sqlite3"; const server = new Server( { name: "executeautomation/database-server", - version: "1.0.0", + version: "1.1.0", }, { capabilities: { diff --git a/package-lock.json b/package-lock.json index de0322c..b8dce20 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "dependencies": { "@modelcontextprotocol/sdk": "1.9.0", "mssql": "11.0.1", + "mysql2": "^3.14.1", "pg": "^8.11.3", "sqlite3": "5.1.7" }, @@ -639,6 +640,14 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, + "node_modules/aws-ssl-profiles": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz", + "integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==", + "engines": { + "node": ">= 6.0.0" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -1124,6 +1133,14 @@ "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", "optional": true }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "engines": { + "node": ">=0.10" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -1645,6 +1662,14 @@ "node": ">=8" } }, + "node_modules/generate-function": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", + "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", + "dependencies": { + "is-property": "^1.0.2" + } + }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -2032,6 +2057,11 @@ "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==" }, + "node_modules/is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==" + }, "node_modules/is-stream": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", @@ -2180,12 +2210,31 @@ "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==" + }, "node_modules/lru-cache": { "version": "10.4.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", "dev": true }, + "node_modules/lru.min": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.2.tgz", + "integrity": "sha512-Nv9KddBcQSlQopmBHXSsZVY5xsdlZkdH/Iey0BlcBYggMd4two7cZnKOK9vmy3nY0O5RGH99z1PCeTpPqszUYg==", + "engines": { + "bun": ">=1.0.0", + "deno": ">=1.30.0", + "node": ">=8.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wellwelwel" + } + }, "node_modules/make-fetch-happen": { "version": "9.1.0", "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-9.1.0.tgz", @@ -2545,6 +2594,44 @@ "node": ">=18" } }, + "node_modules/mysql2": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.14.1.tgz", + "integrity": "sha512-7ytuPQJjQB8TNAYX/H2yhL+iQOnIBjAMam361R7UAL0lOVXWjtdrmoL9HYKqKoLp/8UUTRcvo1QPvK9KL7wA8w==", + "dependencies": { + "aws-ssl-profiles": "^1.1.1", + "denque": "^2.1.0", + "generate-function": "^2.3.1", + "iconv-lite": "^0.6.3", + "long": "^5.2.1", + "lru.min": "^1.0.0", + "named-placeholders": "^1.1.3", + "seq-queue": "^0.0.5", + "sqlstring": "^2.3.2" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/named-placeholders": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.3.tgz", + "integrity": "sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w==", + "dependencies": { + "lru-cache": "^7.14.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/named-placeholders/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "engines": { + "node": ">=12" + } + }, "node_modules/napi-build-utils": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", @@ -3413,6 +3500,11 @@ "node": ">= 18" } }, + "node_modules/seq-queue": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz", + "integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==" + }, "node_modules/serve-static": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", @@ -3688,6 +3780,14 @@ } } }, + "node_modules/sqlstring": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz", + "integrity": "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/ssri": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz", diff --git a/package.json b/package.json index 7007a61..7a95a2d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@executeautomation/database-server", - "version": "1.0.2", + "version": "1.1.0", "description": "MCP server for interacting with SQLite and SQL Server databases by ExecuteAutomation", "license": "MIT", "author": "ExecuteAutomation, Ltd (https://executeautomation.com)", @@ -25,6 +25,7 @@ "dependencies": { "@modelcontextprotocol/sdk": "1.9.0", "mssql": "11.0.1", + "mysql2": "^3.14.1", "pg": "^8.11.3", "sqlite3": "5.1.7" }, diff --git a/readme.md b/readme.md index 62a654d..890f4ac 100644 --- a/readme.md +++ b/readme.md @@ -2,7 +2,7 @@ # MCP Database Server -This MCP (Model Context Protocol) server provides database access capabilities to Claude, supporting SQLite, SQL Server, and PostgreSQL databases. +This MCP (Model Context Protocol) server provides database access capabilities to Claude, supporting SQLite, SQL Server, PostgreSQL, and MySQL databases. ## Installation @@ -92,6 +92,25 @@ Optional parameters: - `--ssl`: Enable SSL connection (true/false) - `--connection-timeout`: Connection timeout in milliseconds (default: 30000) +### MySQL Database + +To use with a MySQL database: + +``` +node dist/src/index.js --mysql --host --database --port [--user --password ] +``` + +Required parameters: +- `--host`: MySQL host name or IP address +- `--database`: Name of the database +- `--port`: Port number (default: 3306) + +Optional parameters: +- `--user`: Username for MySQL authentication +- `--password`: Password for MySQL authentication +- `--ssl`: Enable SSL connection (true/false or object) +- `--connection-timeout`: Connection timeout in milliseconds (default: 30000) + ## Configuring Claude Desktop ### Direct Usage Configuration @@ -132,6 +151,19 @@ If you installed the package globally, configure Claude Desktop with: "--user", "your-username", "--password", "your-password" ] + }, + "mysql": { + "command": "npx", + "args": [ + "-y", + "@executeautomation/database-server", + "--mysql", + "--host", "your-host-name", + "--database", "your-database-name", + "--port", "3306", + "--user", "your-username", + "--password", "your-password" + ] } } } @@ -172,6 +204,18 @@ For local development, configure Claude Desktop to use your locally built versio "--user", "your-username", "--password", "your-password" ] + }, + "mysql": { + "command": "node", + "args": [ + "/absolute/path/to/mcp-database-server/dist/src/index.js", + "--mysql", + "--host", "your-host-name", + "--database", "your-database-name", + "--port", "3306", + "--user", "your-username", + "--password", "your-password" + ] } } } diff --git a/src/db/adapter.ts b/src/db/adapter.ts index 78fbb4e..3ae6dc5 100644 --- a/src/db/adapter.ts +++ b/src/db/adapter.ts @@ -54,6 +54,7 @@ export interface DbAdapter { import { SqliteAdapter } from './sqlite-adapter.js'; import { SqlServerAdapter } from './sqlserver-adapter.js'; import { PostgresqlAdapter } from './postgresql-adapter.js'; +import { MysqlAdapter } from './mysql-adapter.js'; /** * Factory function to create the appropriate database adapter @@ -72,6 +73,8 @@ export function createDbAdapter(type: string, connectionInfo: any): DbAdapter { case 'postgresql': case 'postgres': return new PostgresqlAdapter(connectionInfo); + case 'mysql': + return new MysqlAdapter(connectionInfo); default: throw new Error(`Unsupported database type: ${type}`); } diff --git a/src/db/mysql-adapter.ts b/src/db/mysql-adapter.ts new file mode 100644 index 0000000..0986da4 --- /dev/null +++ b/src/db/mysql-adapter.ts @@ -0,0 +1,145 @@ +import { DbAdapter } from "./adapter.js"; +import mysql from "mysql2/promise"; + +/** + * MySQL database adapter implementation + */ +export class MysqlAdapter implements DbAdapter { + private connection: mysql.Connection | null = null; + private config: mysql.ConnectionOptions; + private host: string; + private database: string; + + constructor(connectionInfo: { + host: string; + database: string; + user?: string; + password?: string; + port?: number; + ssl?: boolean | object; + connectionTimeout?: number; + }) { + this.host = connectionInfo.host; + this.database = connectionInfo.database; + this.config = { + host: connectionInfo.host, + database: connectionInfo.database, + port: connectionInfo.port || 3306, + user: connectionInfo.user, + password: connectionInfo.password, + connectTimeout: connectionInfo.connectionTimeout || 30000, + multipleStatements: true, + }; + if (typeof connectionInfo.ssl === 'object' || typeof connectionInfo.ssl === 'string') { + this.config.ssl = connectionInfo.ssl; + } else if (connectionInfo.ssl === true) { + this.config.ssl = {}; + } + // Validate port + if (connectionInfo.port && typeof connectionInfo.port !== 'number') { + const parsedPort = parseInt(connectionInfo.port as any, 10); + if (isNaN(parsedPort)) { + throw new Error(`Invalid port value for MySQL: ${connectionInfo.port}`); + } + this.config.port = parsedPort; + } + // Log the port for debugging + console.error(`[DEBUG] MySQL connection will use port: ${this.config.port}`); + } + + /** + * Initialize MySQL connection + */ + async init(): Promise { + try { + console.error(`[INFO] Connecting to MySQL: ${this.host}, Database: ${this.database}`); + this.connection = await mysql.createConnection(this.config); + console.error(`[INFO] MySQL connection established successfully`); + } catch (err) { + console.error(`[ERROR] MySQL connection error: ${(err as Error).message}`); + throw new Error(`Failed to connect to MySQL: ${(err as Error).message}`); + } + } + + /** + * Execute a SQL query and get all results + */ + async all(query: string, params: any[] = []): Promise { + if (!this.connection) { + throw new Error("Database not initialized"); + } + try { + const [rows] = await this.connection.execute(query, params); + return Array.isArray(rows) ? rows : []; + } catch (err) { + throw new Error(`MySQL query error: ${(err as Error).message}`); + } + } + + /** + * Execute a SQL query that modifies data + */ + async run(query: string, params: any[] = []): Promise<{ changes: number, lastID: number }> { + if (!this.connection) { + throw new Error("Database not initialized"); + } + try { + const [result]: any = await this.connection.execute(query, params); + const changes = result.affectedRows || 0; + const lastID = result.insertId || 0; + return { changes, lastID }; + } catch (err) { + throw new Error(`MySQL query error: ${(err as Error).message}`); + } + } + + /** + * Execute multiple SQL statements + */ + async exec(query: string): Promise { + if (!this.connection) { + throw new Error("Database not initialized"); + } + try { + await this.connection.query(query); + } catch (err) { + throw new Error(`MySQL batch error: ${(err as Error).message}`); + } + } + + /** + * Close the database connection + */ + async close(): Promise { + if (this.connection) { + await this.connection.end(); + this.connection = null; + } + } + + /** + * Get database metadata + */ + getMetadata(): { name: string; type: string; server: string; database: string } { + return { + name: "MySQL", + type: "mysql", + server: this.host, + database: this.database, + }; + } + + /** + * Get database-specific query for listing tables + */ + getListTablesQuery(): string { + return "SHOW TABLES"; + } + + /** + * Get database-specific query for describing a table + */ + getDescribeTableQuery(tableName: string): string { + return `DESCRIBE \`${tableName}\``; + } +} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 6b29b06..b23d142 100644 --- a/src/index.ts +++ b/src/index.ts @@ -28,7 +28,7 @@ const logger = { const server = new Server( { name: "executeautomation/database-server", - version: "1.0.1", + version: "1.1.0", }, { capabilities: { @@ -45,6 +45,7 @@ if (args.length === 0) { logger.error("Usage for SQLite: node index.js "); logger.error("Usage for SQL Server: node index.js --sqlserver --server --database [--user --password ]"); logger.error("Usage for PostgreSQL: node index.js --postgresql --host --database [--user --password --port ]"); + logger.error("Usage for MySQL: node index.js --mysql --host --database [--user --password --port ]"); process.exit(1); } @@ -120,6 +121,45 @@ else if (args.includes('--postgresql') || args.includes('--postgres')) { logger.error("Error: PostgreSQL requires --host and --database parameters"); process.exit(1); } +} +// Check if using MySQL +else if (args.includes('--mysql')) { + dbType = 'mysql'; + connectionInfo = { + host: '', + database: '', + user: undefined, + password: undefined, + port: undefined, + ssl: undefined, + connectionTimeout: undefined + }; + // Parse MySQL connection parameters + for (let i = 0; i < args.length; i++) { + if (args[i] === '--host' && i + 1 < args.length) { + connectionInfo.host = args[i + 1]; + } else if (args[i] === '--database' && i + 1 < args.length) { + connectionInfo.database = args[i + 1]; + } else if (args[i] === '--user' && i + 1 < args.length) { + connectionInfo.user = args[i + 1]; + } else if (args[i] === '--password' && i + 1 < args.length) { + connectionInfo.password = args[i + 1]; + } else if (args[i] === '--port' && i + 1 < args.length) { + connectionInfo.port = parseInt(args[i + 1], 10); + } else if (args[i] === '--ssl' && i + 1 < args.length) { + const sslVal = args[i + 1]; + if (sslVal === 'true') connectionInfo.ssl = true; + else if (sslVal === 'false') connectionInfo.ssl = false; + else connectionInfo.ssl = sslVal; + } else if (args[i] === '--connection-timeout' && i + 1 < args.length) { + connectionInfo.connectionTimeout = parseInt(args[i + 1], 10); + } + } + // Validate MySQL connection info + if (!connectionInfo.host || !connectionInfo.database) { + logger.error("Error: MySQL requires --host and --database parameters"); + process.exit(1); + } } else { // SQLite mode (default) dbType = 'sqlite'; @@ -178,6 +218,8 @@ async function runServer() { logger.info(`Server: ${connectionInfo.server}, Database: ${connectionInfo.database}`); } else if (dbType === 'postgresql') { logger.info(`Host: ${connectionInfo.host}, Database: ${connectionInfo.database}`); + } else if (dbType === 'mysql') { + logger.info(`Host: ${connectionInfo.host}, Database: ${connectionInfo.database}`); } // Initialize the database