UNPKG

@benborla29/mcp-server-mysql

Version:

MCP server for interacting with MySQL databases based on Node

267 lines (266 loc) 11.3 kB
#!/usr/bin/env node import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { CallToolRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema, } from "@modelcontextprotocol/sdk/types.js"; import { log } from "./src/utils/index.js"; import { ALLOW_DELETE_OPERATION, ALLOW_DDL_OPERATION, ALLOW_INSERT_OPERATION, ALLOW_UPDATE_OPERATION, SCHEMA_DELETE_PERMISSIONS, SCHEMA_DDL_PERMISSIONS, SCHEMA_INSERT_PERMISSIONS, SCHEMA_UPDATE_PERMISSIONS, isMultiDbMode, mcpConfig as config, MCP_VERSION as version, } from "./src/config/index.js"; import { safeExit, getPool, executeQuery, executeReadOnlyQuery, poolPromise, } from "./src/db/index.js"; log("info", `Starting MySQL MCP server v${version}...`); const toolVersion = `MySQL MCP Server [v${process.env.npm_package_version}]`; let toolDescription = `[${toolVersion}] Run SQL queries against MySQL database`; if (isMultiDbMode) { toolDescription += " (Multi-DB mode enabled)"; } if (ALLOW_INSERT_OPERATION || ALLOW_UPDATE_OPERATION || ALLOW_DELETE_OPERATION || ALLOW_DDL_OPERATION) { toolDescription += " with support for:"; if (ALLOW_INSERT_OPERATION) { toolDescription += " INSERT,"; } if (ALLOW_UPDATE_OPERATION) { toolDescription += " UPDATE,"; } if (ALLOW_DELETE_OPERATION) { toolDescription += " DELETE,"; } if (ALLOW_DDL_OPERATION) { toolDescription += " DDL,"; } toolDescription = toolDescription.replace(/,$/, "") + " and READ operations"; if (Object.keys(SCHEMA_INSERT_PERMISSIONS).length > 0 || Object.keys(SCHEMA_UPDATE_PERMISSIONS).length > 0 || Object.keys(SCHEMA_DELETE_PERMISSIONS).length > 0 || Object.keys(SCHEMA_DDL_PERMISSIONS).length > 0) { toolDescription += " (Schema-specific permissions enabled)"; } } else { toolDescription += " (READ-ONLY)"; } log("info", "MySQL Configuration:", JSON.stringify({ ...(process.env.MYSQL_SOCKET_PATH ? { socketPath: process.env.MYSQL_SOCKET_PATH, connectionType: "Unix Socket", } : { host: process.env.MYSQL_HOST || "127.0.0.1", port: process.env.MYSQL_PORT || "3306", connectionType: "TCP/IP", }), user: config.mysql.user, password: config.mysql.password ? "******" : "not set", database: config.mysql.database || "MULTI_DB_MODE", ssl: process.env.MYSQL_SSL === "true" ? "enabled" : "disabled", multiDbMode: isMultiDbMode ? "enabled" : "disabled", }, null, 2)); let serverInstance = null; const getServer = () => { if (!serverInstance) { serverInstance = new Promise((resolve) => { const server = new Server(config.server, { capabilities: { resources: {}, tools: { mysql_query: { description: toolDescription, inputSchema: { type: "object", properties: { sql: { type: "string", description: "The SQL query to execute", }, }, required: ["sql"], }, }, }, }, }); server.setRequestHandler(ListResourcesRequestSchema, async () => { try { log("info", "Handling ListResourcesRequest"); const connectionInfo = process.env.MYSQL_SOCKET_PATH ? `socket:${process.env.MYSQL_SOCKET_PATH}` : `${process.env.MYSQL_HOST || "127.0.0.1"}:${process.env.MYSQL_PORT || "3306"}`; if (isMultiDbMode) { const databases = (await executeQuery("SHOW DATABASES")); let allResources = []; for (const db of databases) { if ([ "information_schema", "mysql", "performance_schema", "sys", ].includes(db.Database)) { continue; } const tables = (await executeQuery(`SELECT table_name FROM information_schema.tables WHERE table_schema = '${db.Database}'`)); allResources.push(...tables.map((row) => ({ uri: new URL(`${db.Database}/${row.table_name}/${config.paths.schema}`, connectionInfo).href, mimeType: "application/json", name: `"${db.Database}.${row.table_name}" database schema`, }))); } return { resources: allResources, }; } else { const results = (await executeQuery("SELECT table_name FROM information_schema.tables WHERE table_schema = DATABASE()")); return { resources: results.map((row) => ({ uri: new URL(`${row.table_name}/${config.paths.schema}`, connectionInfo).href, mimeType: "application/json", name: `"${row.table_name}" database schema`, })), }; } } catch (error) { log("error", "Error in ListResourcesRequest handler:", error); throw error; } }); server.setRequestHandler(ReadResourceRequestSchema, async (request) => { try { log("error", "Handling ReadResourceRequest"); const resourceUrl = new URL(request.params.uri); const pathComponents = resourceUrl.pathname.split("/"); const schema = pathComponents.pop(); const tableName = pathComponents.pop(); let dbName = null; if (isMultiDbMode && pathComponents.length > 0) { dbName = pathComponents.pop() || null; } if (schema !== config.paths.schema) { throw new Error("Invalid resource URI"); } let columnsQuery = "SELECT column_name, data_type FROM information_schema.columns WHERE table_name = ?"; let queryParams = [tableName]; if (dbName) { columnsQuery += " AND table_schema = ?"; queryParams.push(dbName); } const results = (await executeQuery(columnsQuery, queryParams)); return { contents: [ { uri: request.params.uri, mimeType: "application/json", text: JSON.stringify(results, null, 2), }, ], }; } catch (error) { log("error", "Error in ReadResourceRequest handler:", error); throw error; } }); server.setRequestHandler(ListToolsRequestSchema, async () => { log("error", "Handling ListToolsRequest"); const toolsResponse = { tools: [ { name: "mysql_query", description: toolDescription, inputSchema: { type: "object", properties: { sql: { type: "string", description: "The SQL query to execute", }, }, required: ["sql"], }, }, ], }; log("error", "ListToolsRequest response:", JSON.stringify(toolsResponse, null, 2)); return toolsResponse; }); server.setRequestHandler(CallToolRequestSchema, async (request) => { try { log("error", "Handling CallToolRequest:", request.params.name); if (request.params.name !== "mysql_query") { throw new Error(`Unknown tool: ${request.params.name}`); } const sql = request.params.arguments?.sql; return executeReadOnlyQuery(sql); } catch (error) { log("error", "Error in CallToolRequest handler:", error); throw error; } }); resolve(server); }); } return serverInstance; }; async function runServer() { try { log("info", "Attempting to test database connection..."); const pool = await getPool(); const connection = await pool.getConnection(); log("info", "Database connection test successful"); connection.release(); const server = await getServer(); const transport = new StdioServerTransport(); await server.connect(transport); } catch (error) { log("error", "Fatal error during server startup:", error); safeExit(1); } } const shutdown = async (signal) => { log("error", `Received ${signal}. Shutting down...`); try { if (poolPromise) { const pool = await poolPromise; await pool.end(); } } catch (err) { log("error", "Error closing pool:", err); throw err; } }; process.on("SIGINT", async () => { try { await shutdown("SIGINT"); process.exit(0); } catch (err) { log("error", "Error during SIGINT shutdown:", err); safeExit(1); } }); process.on("SIGTERM", async () => { try { await shutdown("SIGTERM"); process.exit(0); } catch (err) { log("error", "Error during SIGTERM shutdown:", err); safeExit(1); } }); process.on("uncaughtException", (error) => { log("error", "Uncaught exception:", error); safeExit(1); }); process.on("unhandledRejection", (reason, promise) => { log("error", "Unhandled rejection at:", promise, "reason:", reason); safeExit(1); }); runServer().catch((error) => { log("error", "Server error:", error); safeExit(1); });