@benborla29/mcp-server-mysql
Version:
MCP server for interacting with MySQL databases based on Node
267 lines (266 loc) • 11.3 kB
JavaScript
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);
});