@cainli/mcp-server-mysql 2.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +21 -0
- package/README.md +835 -0
- package/assets/demo.gif +0 -0
- package/dist/evals.js +32 -0
- package/dist/index.js +396 -0
- package/dist/src/config/index.js +116 -0
- package/dist/src/db/index.js +267 -0
- package/dist/src/db/permissions.js +34 -0
- package/dist/src/db/utils.js +34 -0
- package/dist/src/types/index.js +1 -0
- package/dist/src/utils/index.js +123 -0
- package/package.json +87 -0
package/assets/demo.gif
ADDED
|
Binary file
|
package/dist/evals.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { openai } from "@ai-sdk/openai";
|
|
2
|
+
import { grade } from "mcp-evals";
|
|
3
|
+
const mysqlQueryToolEval = {
|
|
4
|
+
name: 'mysql_query Tool Evaluation',
|
|
5
|
+
description: 'Evaluates the MySQL query execution functionality',
|
|
6
|
+
run: async () => {
|
|
7
|
+
const result = await grade(openai("gpt-4"), "Please execute the following SQL query and return the results: SELECT * FROM employees WHERE status='ACTIVE';");
|
|
8
|
+
return JSON.parse(result);
|
|
9
|
+
}
|
|
10
|
+
};
|
|
11
|
+
const mysqlQueryGenerationEval = {
|
|
12
|
+
name: 'mysql_query Tool Generation Evaluation',
|
|
13
|
+
description: 'Evaluates the MySQL query tool for correct SQL generation and execution',
|
|
14
|
+
run: async () => {
|
|
15
|
+
const result = await grade(openai("gpt-4"), "Use the mysql_query tool to select all rows from the 'users' table where isActive = 1. Provide the SQL query in the correct format.");
|
|
16
|
+
return JSON.parse(result);
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
const mysqlQueryColumnsEval = {
|
|
20
|
+
name: 'mysql_query Columns Evaluation',
|
|
21
|
+
description: 'Evaluates the mysql_query tool for column selection',
|
|
22
|
+
run: async () => {
|
|
23
|
+
const result = await grade(openai("gpt-4"), "Please provide a SQL query to retrieve the id, name, and email columns for all records in the users table.");
|
|
24
|
+
return JSON.parse(result);
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
const config = {
|
|
28
|
+
model: openai("gpt-4"),
|
|
29
|
+
evals: [mysqlQueryToolEval, mysqlQueryGenerationEval, mysqlQueryColumnsEval]
|
|
30
|
+
};
|
|
31
|
+
export default config;
|
|
32
|
+
export const evals = [mysqlQueryToolEval, mysqlQueryGenerationEval, mysqlQueryColumnsEval];
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
5
|
+
import { CallToolRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
import { log } from "./src/utils/index.js";
|
|
8
|
+
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, MCP_VERSION as version, IS_REMOTE_MCP, REMOTE_SECRET_KEY, PORT, } from "./src/config/index.js";
|
|
9
|
+
import { safeExit, getPool, executeQuery, executeReadOnlyQuery, poolPromise, } from "./src/db/index.js";
|
|
10
|
+
import express from "express";
|
|
11
|
+
import { fileURLToPath } from 'url';
|
|
12
|
+
import { realpathSync } from 'fs';
|
|
13
|
+
log("info", `Starting MySQL MCP server v${version}...`);
|
|
14
|
+
const toolVersion = `MySQL MCP Server [v${process.env.npm_package_version}]`;
|
|
15
|
+
let toolDescription = `[${toolVersion}] Run SQL queries against MySQL database`;
|
|
16
|
+
if (isMultiDbMode) {
|
|
17
|
+
toolDescription += " (Multi-DB mode enabled)";
|
|
18
|
+
}
|
|
19
|
+
if (ALLOW_INSERT_OPERATION ||
|
|
20
|
+
ALLOW_UPDATE_OPERATION ||
|
|
21
|
+
ALLOW_DELETE_OPERATION ||
|
|
22
|
+
ALLOW_DDL_OPERATION) {
|
|
23
|
+
toolDescription += " with support for:";
|
|
24
|
+
if (ALLOW_INSERT_OPERATION) {
|
|
25
|
+
toolDescription += " INSERT,";
|
|
26
|
+
}
|
|
27
|
+
if (ALLOW_UPDATE_OPERATION) {
|
|
28
|
+
toolDescription += " UPDATE,";
|
|
29
|
+
}
|
|
30
|
+
if (ALLOW_DELETE_OPERATION) {
|
|
31
|
+
toolDescription += " DELETE,";
|
|
32
|
+
}
|
|
33
|
+
if (ALLOW_DDL_OPERATION) {
|
|
34
|
+
toolDescription += " DDL,";
|
|
35
|
+
}
|
|
36
|
+
toolDescription = toolDescription.replace(/,$/, "") + " and READ operations";
|
|
37
|
+
if (Object.keys(SCHEMA_INSERT_PERMISSIONS).length > 0 ||
|
|
38
|
+
Object.keys(SCHEMA_UPDATE_PERMISSIONS).length > 0 ||
|
|
39
|
+
Object.keys(SCHEMA_DELETE_PERMISSIONS).length > 0 ||
|
|
40
|
+
Object.keys(SCHEMA_DDL_PERMISSIONS).length > 0) {
|
|
41
|
+
toolDescription += " (Schema-specific permissions enabled)";
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
toolDescription += " (READ-ONLY)";
|
|
46
|
+
}
|
|
47
|
+
const isReadOnly = !(ALLOW_INSERT_OPERATION ||
|
|
48
|
+
ALLOW_UPDATE_OPERATION ||
|
|
49
|
+
ALLOW_DELETE_OPERATION ||
|
|
50
|
+
ALLOW_DDL_OPERATION);
|
|
51
|
+
log("info", "MySQL Configuration:", JSON.stringify({
|
|
52
|
+
...(process.env.MYSQL_SOCKET_PATH
|
|
53
|
+
? {
|
|
54
|
+
socketPath: process.env.MYSQL_SOCKET_PATH,
|
|
55
|
+
connectionType: "Unix Socket",
|
|
56
|
+
}
|
|
57
|
+
: {
|
|
58
|
+
host: process.env.MYSQL_HOST || "127.0.0.1",
|
|
59
|
+
port: process.env.MYSQL_PORT || "3306",
|
|
60
|
+
connectionType: "TCP/IP",
|
|
61
|
+
}),
|
|
62
|
+
user: mcpConfig.mysql.user,
|
|
63
|
+
password: mcpConfig.mysql.password ? "******" : "not set",
|
|
64
|
+
database: mcpConfig.mysql.database || "MULTI_DB_MODE",
|
|
65
|
+
ssl: process.env.MYSQL_SSL === "true" ? "enabled" : "disabled",
|
|
66
|
+
sslCA: process.env.MYSQL_SSL_CA || "not set",
|
|
67
|
+
sslCert: process.env.MYSQL_SSL_CERT || "not set",
|
|
68
|
+
sslKey: process.env.MYSQL_SSL_KEY || "not set",
|
|
69
|
+
multiDbMode: isMultiDbMode ? "enabled" : "disabled",
|
|
70
|
+
}, null, 2));
|
|
71
|
+
export const configSchema = z.object({
|
|
72
|
+
debug: z.boolean().default(false).describe("Enable debug logging"),
|
|
73
|
+
});
|
|
74
|
+
export default function createMcpServer({ sessionId, config, }) {
|
|
75
|
+
const server = new Server({
|
|
76
|
+
name: "MySQL MCP Server",
|
|
77
|
+
version: process.env.npm_package_version || "1.0.0",
|
|
78
|
+
}, {
|
|
79
|
+
capabilities: {
|
|
80
|
+
resources: {},
|
|
81
|
+
tools: {
|
|
82
|
+
mysql_query: {
|
|
83
|
+
description: toolDescription,
|
|
84
|
+
inputSchema: {
|
|
85
|
+
type: "object",
|
|
86
|
+
properties: {
|
|
87
|
+
sql: {
|
|
88
|
+
type: "string",
|
|
89
|
+
description: "The SQL query to execute",
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
required: ["sql"],
|
|
93
|
+
},
|
|
94
|
+
annotations: {
|
|
95
|
+
readOnlyHint: isReadOnly,
|
|
96
|
+
idempotentHint: isReadOnly,
|
|
97
|
+
destructiveHint: !isReadOnly,
|
|
98
|
+
openWorldHint: false,
|
|
99
|
+
title: "MySQL Query",
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
server.setRequestHandler(ListResourcesRequestSchema, async () => {
|
|
106
|
+
try {
|
|
107
|
+
log("info", "Handling ListResourcesRequest");
|
|
108
|
+
const connectionInfo = process.env.MYSQL_SOCKET_PATH
|
|
109
|
+
? `socket: ${process.env.MYSQL_SOCKET_PATH}`
|
|
110
|
+
: `host: ${process.env.MYSQL_HOST || "localhost"}, port: ${process.env.MYSQL_PORT || 3306}`;
|
|
111
|
+
log("info", `Connection info: ${connectionInfo}`);
|
|
112
|
+
const tablesQuery = `
|
|
113
|
+
SELECT
|
|
114
|
+
table_name as name,
|
|
115
|
+
table_schema as \`database\`,
|
|
116
|
+
table_comment as description,
|
|
117
|
+
table_rows as rowCount,
|
|
118
|
+
data_length as dataSize,
|
|
119
|
+
index_length as indexSize,
|
|
120
|
+
create_time as createTime,
|
|
121
|
+
update_time as updateTime
|
|
122
|
+
FROM
|
|
123
|
+
information_schema.tables
|
|
124
|
+
WHERE
|
|
125
|
+
table_schema NOT IN ('information_schema', 'mysql', 'performance_schema', 'sys')
|
|
126
|
+
${!isMultiDbMode && mcpConfig.mysql.database ? `AND table_schema = '${mcpConfig.mysql.database}'` : ''}
|
|
127
|
+
ORDER BY
|
|
128
|
+
table_schema, table_name
|
|
129
|
+
`;
|
|
130
|
+
const queryResult = (await executeReadOnlyQuery(tablesQuery));
|
|
131
|
+
const tables = JSON.parse(queryResult.content[0].text);
|
|
132
|
+
log("info", `Found ${tables.length} tables`);
|
|
133
|
+
const resources = tables.map((table) => ({
|
|
134
|
+
uri: `mysql://tables/${table.name}`,
|
|
135
|
+
name: table.name,
|
|
136
|
+
title: `${table.database}.${table.name}`,
|
|
137
|
+
description: table.description ||
|
|
138
|
+
`Table ${table.name} in database ${table.database}`,
|
|
139
|
+
mimeType: "application/json",
|
|
140
|
+
}));
|
|
141
|
+
resources.push({
|
|
142
|
+
uri: "mysql://tables",
|
|
143
|
+
name: "Tables",
|
|
144
|
+
title: "MySQL Tables",
|
|
145
|
+
description: "List of all MySQL tables",
|
|
146
|
+
mimeType: "application/json",
|
|
147
|
+
});
|
|
148
|
+
return { resources };
|
|
149
|
+
}
|
|
150
|
+
catch (error) {
|
|
151
|
+
log("error", "Error in ListResourcesRequest handler:", error);
|
|
152
|
+
throw error;
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
156
|
+
try {
|
|
157
|
+
log("info", "Handling ReadResourceRequest:", request.params.uri);
|
|
158
|
+
const uriParts = request.params.uri.split("/");
|
|
159
|
+
const tableName = uriParts.pop();
|
|
160
|
+
const dbName = uriParts.length > 0 ? uriParts.pop() : null;
|
|
161
|
+
if (!tableName) {
|
|
162
|
+
throw new Error(`Invalid resource URI: ${request.params.uri}`);
|
|
163
|
+
}
|
|
164
|
+
let columnsQuery = "SELECT column_name, data_type FROM information_schema.columns WHERE table_name = ?";
|
|
165
|
+
let queryParams = [tableName];
|
|
166
|
+
const schemaName = dbName || (!isMultiDbMode ? mcpConfig.mysql.database : null);
|
|
167
|
+
if (schemaName) {
|
|
168
|
+
columnsQuery += " AND table_schema = ?";
|
|
169
|
+
queryParams.push(schemaName);
|
|
170
|
+
}
|
|
171
|
+
const results = (await executeQuery(columnsQuery, queryParams));
|
|
172
|
+
return {
|
|
173
|
+
contents: [
|
|
174
|
+
{
|
|
175
|
+
uri: request.params.uri,
|
|
176
|
+
mimeType: "application/json",
|
|
177
|
+
text: JSON.stringify(results, null, 2),
|
|
178
|
+
},
|
|
179
|
+
],
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
catch (error) {
|
|
183
|
+
log("error", "Error in ReadResourceRequest handler:", error);
|
|
184
|
+
throw error;
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
188
|
+
try {
|
|
189
|
+
log("info", "Handling CallToolRequest:", request.params.name);
|
|
190
|
+
if (request.params.name !== "mysql_query") {
|
|
191
|
+
throw new Error(`Unknown tool: ${request.params.name}`);
|
|
192
|
+
}
|
|
193
|
+
const sql = request.params.arguments?.sql;
|
|
194
|
+
return await executeReadOnlyQuery(sql);
|
|
195
|
+
}
|
|
196
|
+
catch (err) {
|
|
197
|
+
const error = err;
|
|
198
|
+
log("error", "Error in CallToolRequest handler:", error);
|
|
199
|
+
return {
|
|
200
|
+
content: [{
|
|
201
|
+
type: "text",
|
|
202
|
+
text: `Error: ${error.message}`
|
|
203
|
+
}],
|
|
204
|
+
isError: true
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
209
|
+
log("info", "Handling ListToolsRequest");
|
|
210
|
+
const toolsResponse = {
|
|
211
|
+
tools: [
|
|
212
|
+
{
|
|
213
|
+
name: "mysql_query",
|
|
214
|
+
description: toolDescription,
|
|
215
|
+
inputSchema: {
|
|
216
|
+
type: "object",
|
|
217
|
+
properties: {
|
|
218
|
+
sql: {
|
|
219
|
+
type: "string",
|
|
220
|
+
description: "The SQL query to execute",
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
required: ["sql"],
|
|
224
|
+
},
|
|
225
|
+
annotations: {
|
|
226
|
+
readOnlyHint: isReadOnly,
|
|
227
|
+
idempotentHint: isReadOnly,
|
|
228
|
+
destructiveHint: !isReadOnly,
|
|
229
|
+
openWorldHint: false,
|
|
230
|
+
title: "MySQL Query",
|
|
231
|
+
},
|
|
232
|
+
},
|
|
233
|
+
],
|
|
234
|
+
};
|
|
235
|
+
log("info", "ListToolsRequest response:", JSON.stringify(toolsResponse, null, 2));
|
|
236
|
+
return toolsResponse;
|
|
237
|
+
});
|
|
238
|
+
(async () => {
|
|
239
|
+
try {
|
|
240
|
+
log("info", "Attempting to test database connection...");
|
|
241
|
+
const pool = await getPool();
|
|
242
|
+
const connection = await pool.getConnection();
|
|
243
|
+
log("info", "Database connection test successful");
|
|
244
|
+
connection.release();
|
|
245
|
+
}
|
|
246
|
+
catch (error) {
|
|
247
|
+
log("error", "Fatal error during server startup:", error);
|
|
248
|
+
safeExit(1);
|
|
249
|
+
}
|
|
250
|
+
})();
|
|
251
|
+
const shutdown = async (signal) => {
|
|
252
|
+
log("error", `Received ${signal}. Shutting down...`);
|
|
253
|
+
try {
|
|
254
|
+
if (poolPromise) {
|
|
255
|
+
const pool = await poolPromise;
|
|
256
|
+
await pool.end();
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
catch (err) {
|
|
260
|
+
log("error", "Error closing pool:", err);
|
|
261
|
+
throw err;
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
process.on("SIGINT", async () => {
|
|
265
|
+
try {
|
|
266
|
+
await shutdown("SIGINT");
|
|
267
|
+
process.exit(0);
|
|
268
|
+
}
|
|
269
|
+
catch (err) {
|
|
270
|
+
log("error", "Error during SIGINT shutdown:", err);
|
|
271
|
+
safeExit(1);
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
process.on("SIGTERM", async () => {
|
|
275
|
+
try {
|
|
276
|
+
await shutdown("SIGTERM");
|
|
277
|
+
process.exit(0);
|
|
278
|
+
}
|
|
279
|
+
catch (err) {
|
|
280
|
+
log("error", "Error during SIGTERM shutdown:", err);
|
|
281
|
+
safeExit(1);
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
process.on("uncaughtException", (error) => {
|
|
285
|
+
log("error", "Uncaught exception:", error);
|
|
286
|
+
safeExit(1);
|
|
287
|
+
});
|
|
288
|
+
process.on("unhandledRejection", (reason, promise) => {
|
|
289
|
+
log("error", "Unhandled rejection at:", promise, "reason:", reason);
|
|
290
|
+
safeExit(1);
|
|
291
|
+
});
|
|
292
|
+
return server;
|
|
293
|
+
}
|
|
294
|
+
const isMainModule = () => {
|
|
295
|
+
if (typeof require !== 'undefined' && require.main === module) {
|
|
296
|
+
return true;
|
|
297
|
+
}
|
|
298
|
+
if (typeof import.meta !== 'undefined' && import.meta.url && process.argv[1]) {
|
|
299
|
+
const currentModulePath = fileURLToPath(import.meta.url);
|
|
300
|
+
const mainScriptPath = realpathSync(process.argv[1]);
|
|
301
|
+
return currentModulePath === mainScriptPath;
|
|
302
|
+
}
|
|
303
|
+
return false;
|
|
304
|
+
};
|
|
305
|
+
if (isMainModule()) {
|
|
306
|
+
log("info", "Running in standalone mode");
|
|
307
|
+
(async () => {
|
|
308
|
+
try {
|
|
309
|
+
const mcpServer = createMcpServer({ config: { debug: false } });
|
|
310
|
+
if (IS_REMOTE_MCP && REMOTE_SECRET_KEY?.length) {
|
|
311
|
+
const app = express();
|
|
312
|
+
app.use(express.json());
|
|
313
|
+
app.post("/mcp", async (req, res) => {
|
|
314
|
+
if (!req.get("Authorization") ||
|
|
315
|
+
!req.get("Authorization")?.startsWith("Bearer ") ||
|
|
316
|
+
!req.get("Authorization")?.endsWith(REMOTE_SECRET_KEY)) {
|
|
317
|
+
console.error("Missing or invalid Authorization header");
|
|
318
|
+
res.status(401).json({
|
|
319
|
+
jsonrpc: "2.0",
|
|
320
|
+
error: {
|
|
321
|
+
code: -32603,
|
|
322
|
+
message: "Missing or invalid Authorization header",
|
|
323
|
+
},
|
|
324
|
+
id: null,
|
|
325
|
+
});
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
try {
|
|
329
|
+
const server = mcpServer;
|
|
330
|
+
const transport = new StreamableHTTPServerTransport({
|
|
331
|
+
sessionIdGenerator: undefined,
|
|
332
|
+
});
|
|
333
|
+
res.on("close", () => {
|
|
334
|
+
log("info", "Request closed");
|
|
335
|
+
transport.close();
|
|
336
|
+
server.close();
|
|
337
|
+
});
|
|
338
|
+
await server.connect(transport);
|
|
339
|
+
await transport.handleRequest(req, res, req.body);
|
|
340
|
+
}
|
|
341
|
+
catch (error) {
|
|
342
|
+
log("error", "Error handling MCP request:", error);
|
|
343
|
+
if (!res.headersSent) {
|
|
344
|
+
res.status(500).json({
|
|
345
|
+
jsonrpc: "2.0",
|
|
346
|
+
error: {
|
|
347
|
+
code: -32603,
|
|
348
|
+
message: error.message,
|
|
349
|
+
},
|
|
350
|
+
id: null,
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
});
|
|
355
|
+
app.get("/mcp", async (req, res) => {
|
|
356
|
+
console.log("Received GET MCP request");
|
|
357
|
+
res.writeHead(405).end(JSON.stringify({
|
|
358
|
+
jsonrpc: "2.0",
|
|
359
|
+
error: {
|
|
360
|
+
code: -32000,
|
|
361
|
+
message: "Method not allowed.",
|
|
362
|
+
},
|
|
363
|
+
id: null,
|
|
364
|
+
}));
|
|
365
|
+
});
|
|
366
|
+
app.delete("/mcp", async (req, res) => {
|
|
367
|
+
console.log("Received DELETE MCP request");
|
|
368
|
+
res.writeHead(405).end(JSON.stringify({
|
|
369
|
+
jsonrpc: "2.0",
|
|
370
|
+
error: {
|
|
371
|
+
code: -32000,
|
|
372
|
+
message: "Method not allowed.",
|
|
373
|
+
},
|
|
374
|
+
id: null,
|
|
375
|
+
}));
|
|
376
|
+
});
|
|
377
|
+
app.listen(PORT, (error) => {
|
|
378
|
+
if (error) {
|
|
379
|
+
console.error("Failed to start server:", error);
|
|
380
|
+
process.exit(1);
|
|
381
|
+
}
|
|
382
|
+
console.log(`MCP Stateless Streamable HTTP Server listening on port ${PORT}`);
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
else {
|
|
386
|
+
const transport = new StdioServerTransport();
|
|
387
|
+
await mcpServer.connect(transport);
|
|
388
|
+
log("info", "Server started and listening on stdio");
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
catch (error) {
|
|
392
|
+
log("error", "Server error:", error);
|
|
393
|
+
safeExit(1);
|
|
394
|
+
}
|
|
395
|
+
})();
|
|
396
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import * as dotenv from "dotenv";
|
|
2
|
+
import * as fs from "fs";
|
|
3
|
+
import { parseSchemaPermissions, parseMySQLConnectionString } from "../utils/index.js";
|
|
4
|
+
function readSSLFile(filePath, label) {
|
|
5
|
+
try {
|
|
6
|
+
if (!fs.existsSync(filePath)) {
|
|
7
|
+
throw new Error(`SSL ${label} file not found: ${filePath}`);
|
|
8
|
+
}
|
|
9
|
+
const data = fs.readFileSync(filePath);
|
|
10
|
+
if (data.length === 0) {
|
|
11
|
+
throw new Error(`SSL ${label} file is empty: ${filePath}`);
|
|
12
|
+
}
|
|
13
|
+
return data;
|
|
14
|
+
}
|
|
15
|
+
catch (error) {
|
|
16
|
+
if (error instanceof Error) {
|
|
17
|
+
if (error.message.startsWith('SSL ')) {
|
|
18
|
+
throw error;
|
|
19
|
+
}
|
|
20
|
+
throw new Error(`Failed to read SSL ${label}: ${error.message}`);
|
|
21
|
+
}
|
|
22
|
+
throw error;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function readCACertificate(filePath) {
|
|
26
|
+
return readSSLFile(filePath, 'CA certificate');
|
|
27
|
+
}
|
|
28
|
+
export const MCP_VERSION = "2.0.2";
|
|
29
|
+
dotenv.config();
|
|
30
|
+
const connectionStringConfig = process.env.MYSQL_CONNECTION_STRING
|
|
31
|
+
? parseMySQLConnectionString(process.env.MYSQL_CONNECTION_STRING)
|
|
32
|
+
: {};
|
|
33
|
+
if (process.env.NODE_ENV === "test" && !process.env.MYSQL_DB) {
|
|
34
|
+
process.env.MYSQL_DB = "mcp_test_db";
|
|
35
|
+
}
|
|
36
|
+
export const ALLOW_INSERT_OPERATION = process.env.ALLOW_INSERT_OPERATION === "true";
|
|
37
|
+
export const ALLOW_UPDATE_OPERATION = process.env.ALLOW_UPDATE_OPERATION === "true";
|
|
38
|
+
export const ALLOW_DELETE_OPERATION = process.env.ALLOW_DELETE_OPERATION === "true";
|
|
39
|
+
export const ALLOW_DDL_OPERATION = process.env.ALLOW_DDL_OPERATION === "true";
|
|
40
|
+
export const MYSQL_DISABLE_READ_ONLY_TRANSACTIONS = process.env.MYSQL_DISABLE_READ_ONLY_TRANSACTIONS === "true";
|
|
41
|
+
export const SCHEMA_INSERT_PERMISSIONS = parseSchemaPermissions(process.env.SCHEMA_INSERT_PERMISSIONS);
|
|
42
|
+
export const SCHEMA_UPDATE_PERMISSIONS = parseSchemaPermissions(process.env.SCHEMA_UPDATE_PERMISSIONS);
|
|
43
|
+
export const SCHEMA_DELETE_PERMISSIONS = parseSchemaPermissions(process.env.SCHEMA_DELETE_PERMISSIONS);
|
|
44
|
+
export const SCHEMA_DDL_PERMISSIONS = parseSchemaPermissions(process.env.SCHEMA_DDL_PERMISSIONS);
|
|
45
|
+
export const IS_REMOTE_MCP = process.env.IS_REMOTE_MCP === "true";
|
|
46
|
+
export const REMOTE_SECRET_KEY = process.env.REMOTE_SECRET_KEY || "";
|
|
47
|
+
export const PORT = process.env.PORT || 3000;
|
|
48
|
+
const dbFromEnvOrConnString = connectionStringConfig.database || process.env.MYSQL_DB;
|
|
49
|
+
export const isMultiDbMode = !dbFromEnvOrConnString || dbFromEnvOrConnString.trim() === "";
|
|
50
|
+
export const mcpConfig = {
|
|
51
|
+
server: {
|
|
52
|
+
name: "@benborla29/mcp-server-mysql",
|
|
53
|
+
version: MCP_VERSION,
|
|
54
|
+
connectionTypes: ["stdio", "streamableHttp"],
|
|
55
|
+
},
|
|
56
|
+
mysql: {
|
|
57
|
+
...(connectionStringConfig.socketPath || process.env.MYSQL_SOCKET_PATH
|
|
58
|
+
? {
|
|
59
|
+
socketPath: connectionStringConfig.socketPath || process.env.MYSQL_SOCKET_PATH,
|
|
60
|
+
}
|
|
61
|
+
: {
|
|
62
|
+
host: connectionStringConfig.host || process.env.MYSQL_HOST || "127.0.0.1",
|
|
63
|
+
port: connectionStringConfig.port || Number(process.env.MYSQL_PORT || "3306"),
|
|
64
|
+
}),
|
|
65
|
+
user: connectionStringConfig.user || process.env.MYSQL_USER || "root",
|
|
66
|
+
password: connectionStringConfig.password !== undefined
|
|
67
|
+
? connectionStringConfig.password
|
|
68
|
+
: process.env.MYSQL_PASS === undefined
|
|
69
|
+
? ""
|
|
70
|
+
: process.env.MYSQL_PASS,
|
|
71
|
+
database: connectionStringConfig.database || process.env.MYSQL_DB || undefined,
|
|
72
|
+
connectionLimit: 10,
|
|
73
|
+
waitForConnections: true,
|
|
74
|
+
queueLimit: process.env.MYSQL_QUEUE_LIMIT ? parseInt(process.env.MYSQL_QUEUE_LIMIT, 10) : 100,
|
|
75
|
+
enableKeepAlive: true,
|
|
76
|
+
keepAliveInitialDelay: 0,
|
|
77
|
+
connectTimeout: process.env.MYSQL_CONNECT_TIMEOUT ? parseInt(process.env.MYSQL_CONNECT_TIMEOUT, 10) : 10000,
|
|
78
|
+
authPlugins: {
|
|
79
|
+
mysql_clear_password: () => () => Buffer.from(connectionStringConfig.password !== undefined
|
|
80
|
+
? connectionStringConfig.password
|
|
81
|
+
: process.env.MYSQL_PASS !== undefined
|
|
82
|
+
? process.env.MYSQL_PASS
|
|
83
|
+
: ""),
|
|
84
|
+
},
|
|
85
|
+
...(process.env.MYSQL_SSL === "true"
|
|
86
|
+
? {
|
|
87
|
+
ssl: {
|
|
88
|
+
rejectUnauthorized: process.env.MYSQL_SSL_REJECT_UNAUTHORIZED === "true",
|
|
89
|
+
...(process.env.MYSQL_SSL_CA
|
|
90
|
+
? { ca: readCACertificate(process.env.MYSQL_SSL_CA) }
|
|
91
|
+
: {}),
|
|
92
|
+
...(process.env.MYSQL_SSL_CERT
|
|
93
|
+
? { cert: readSSLFile(process.env.MYSQL_SSL_CERT, 'client certificate') }
|
|
94
|
+
: {}),
|
|
95
|
+
...(process.env.MYSQL_SSL_KEY
|
|
96
|
+
? { key: readSSLFile(process.env.MYSQL_SSL_KEY, 'client private key') }
|
|
97
|
+
: {}),
|
|
98
|
+
},
|
|
99
|
+
}
|
|
100
|
+
: {}),
|
|
101
|
+
...(process.env.MYSQL_TIMEZONE
|
|
102
|
+
? {
|
|
103
|
+
timezone: process.env.MYSQL_TIMEZONE,
|
|
104
|
+
}
|
|
105
|
+
: {}),
|
|
106
|
+
...(process.env.MYSQL_DATE_STRINGS === "true"
|
|
107
|
+
? {
|
|
108
|
+
dateStrings: true,
|
|
109
|
+
}
|
|
110
|
+
: {}),
|
|
111
|
+
},
|
|
112
|
+
paths: {
|
|
113
|
+
schema: "schema",
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
export { readCACertificate, readSSLFile };
|