# Directory Structure
```
├── .gitignore
├── Dockerfile
├── LICENSE
├── package-lock.json
├── package.json
├── README.md
├── smithery.yaml
├── src
│ └── index.ts
└── tsconfig.json
```
# Files
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "compilerOptions": {
3 | "target": "ES2022",
4 | "module": "Node16",
5 | "moduleResolution": "Node16",
6 | "outDir": "./build",
7 | "rootDir": "./src",
8 | "strict": true,
9 | "esModuleInterop": true,
10 | "skipLibCheck": true,
11 | "forceConsistentCasingInFileNames": true
12 | },
13 | "include": ["src/**/*"],
14 | "exclude": ["node_modules"]
15 | }
16 |
```
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
```dockerfile
1 | # Generated by https://smithery.ai. See: https://smithery.ai/docs/config#dockerfile
2 | FROM node:lts-alpine
3 |
4 | WORKDIR /app
5 |
6 | # Copy package.json and package-lock.json
7 | COPY package*.json ./
8 |
9 | # Install dependencies without running prepare scripts
10 | RUN npm install --ignore-scripts
11 |
12 | # Copy the rest of the repository
13 | COPY . .
14 |
15 | # Build the project using TypeScript (tsconfig.json is now available)
16 | RUN npm run build
17 |
18 | # Expose any necessary ports (if applicable)
19 | # EXPOSE 3000
20 |
21 | # Set the entrypoint command to run the MCP server
22 | CMD ["node", "build/index.js"]
23 |
```
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "name": "@malove86/mcp-mysql-server",
3 | "version": "0.2.4",
4 | "description": "A Model Context Protocol server for MySQL database operations",
5 | "type": "module",
6 | "bin": {
7 | "mcp-mysql": "./build/index.js"
8 | },
9 | "files": [
10 | "build"
11 | ],
12 | "scripts": {
13 | "build": "tsc && node -e \"require('fs').chmodSync('build/index.js', '755')\"",
14 | "prepare": "npm run build",
15 | "watch": "tsc --watch",
16 | "inspector": "npx @modelcontextprotocol/inspector build/index.js"
17 | },
18 | "keywords": [
19 | "mcp",
20 | "model-context-protocol",
21 | "mysql",
22 | "database",
23 | "claude",
24 | "anthropic"
25 | ],
26 | "author": "malove86",
27 | "license": "MIT",
28 | "dependencies": {
29 | "@modelcontextprotocol/sdk": "0.6.0",
30 | "dotenv": "^16.4.7",
31 | "mysql2": "^3.11.5"
32 | },
33 | "devDependencies": {
34 | "@types/node": "^20.11.24",
35 | "typescript": "^5.3.3"
36 | },
37 | "repository": {
38 | "type": "git",
39 | "url": "https://github.com/Malove86/mcp-mysql-server.git"
40 | },
41 | "publishConfig": {
42 | "access": "public"
43 | }
44 | }
45 |
```
--------------------------------------------------------------------------------
/smithery.yaml:
--------------------------------------------------------------------------------
```yaml
1 | # Smithery configuration file: https://smithery.ai/docs/config#smitheryyaml
2 |
3 | startCommand:
4 | type: stdio
5 | configSchema:
6 | # JSON Schema defining the configuration options for the MCP.
7 | type: object
8 | required:
9 | - mysqlHost
10 | - mysqlUser
11 | - mysqlPassword
12 | - mysqlDatabase
13 | properties:
14 | mysqlHost:
15 | type: string
16 | description: Hostname for the MySQL server
17 | mysqlUser:
18 | type: string
19 | description: MySQL user name
20 | mysqlPassword:
21 | type: string
22 | description: MySQL user's password
23 | mysqlDatabase:
24 | type: string
25 | description: MySQL database name
26 | mysqlPort:
27 | type: integer
28 | description: Port for the MySQL server (default 3306)
29 | default: 3306
30 | commandFunction:
31 | # A JS function that produces the CLI command based on the given config to start the MCP on stdio.
32 | |-
33 | (config) => ({
34 | command: 'node',
35 | args: ['build/index.js'],
36 | env: {
37 | MYSQL_HOST: config.mysqlHost,
38 | MYSQL_USER: config.mysqlUser,
39 | MYSQL_PASSWORD: config.mysqlPassword,
40 | MYSQL_DATABASE: config.mysqlDatabase,
41 | MYSQL_PORT: config.mysqlPort?.toString() || '3306'
42 | }
43 | })
44 | exampleConfig:
45 | mysqlHost: localhost
46 | mysqlUser: example_user
47 | mysqlPassword: example_password
48 | mysqlDatabase: example_db
49 | mysqlPort: 3306
50 |
```
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
```typescript
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 {
5 | CallToolRequestSchema,
6 | ErrorCode,
7 | ListToolsRequestSchema,
8 | McpError,
9 | } from '@modelcontextprotocol/sdk/types.js';
10 | import * as mysql from 'mysql2/promise';
11 | import { config } from 'dotenv';
12 | import { Pool } from 'mysql2/promise';
13 |
14 | // Load environment variables
15 | config();
16 |
17 | interface DatabaseConfig {
18 | host: string;
19 | user: string;
20 | password: string;
21 | database: string;
22 | port?: number; // Add optional port parameter
23 | }
24 |
25 | // Type guard for error objects
26 | function isErrorWithMessage(error: unknown): error is { message: string } {
27 | return (
28 | typeof error === 'object' &&
29 | error !== null &&
30 | 'message' in error &&
31 | typeof (error as Record<string, unknown>).message === 'string'
32 | );
33 | }
34 |
35 | // Helper to get error message
36 | function getErrorMessage(error: unknown): string {
37 | if (isErrorWithMessage(error)) {
38 | return error.message;
39 | }
40 | return String(error);
41 | }
42 |
43 | // Helper to validate SQL query for basic security
44 | function validateSqlQuery(sql: string): boolean {
45 | // 检查是否存在常见SQL注入模式
46 | const dangerousPatterns = [
47 | /;\s*DROP\s+/i,
48 | /;\s*DELETE\s+/i,
49 | /;\s*UPDATE\s+/i,
50 | /;\s*INSERT\s+/i,
51 | /UNION\s+SELECT/i,
52 | /--/,
53 | /\/\*/,
54 | /xp_cmdshell/i
55 | ];
56 |
57 | return !dangerousPatterns.some(pattern => pattern.test(sql));
58 | }
59 |
60 | class MySQLServer {
61 | private server: Server;
62 | private config: DatabaseConfig | null = null;
63 | private pool: Pool | null = null;
64 | private isConnected: boolean = false;
65 | private connectionPromise: Promise<void> | null = null;
66 | private activeConnections: number = 0;
67 | private maxConnections: number = 50; // 最大同时处理的连接数
68 |
69 | constructor() {
70 | this.server = new Server(
71 | {
72 | name: 'mysql-server',
73 | version: '1.0.0',
74 | },
75 | {
76 | capabilities: {
77 | tools: {},
78 | },
79 | }
80 | );
81 |
82 | if (process.env.MYSQL_HOST
83 | && process.env.MYSQL_USER
84 | && process.env.MYSQL_PASSWORD !== undefined
85 | && process.env.MYSQL_PASSWORD !== null
86 | && process.env.MYSQL_DATABASE) {
87 | this.config = {
88 | host: process.env.MYSQL_HOST as string,
89 | user: process.env.MYSQL_USER as string,
90 | password: process.env.MYSQL_PASSWORD as string,
91 | database: process.env.MYSQL_DATABASE as string,
92 | port: Number(process.env.MYSQL_PORT ?? 3306),
93 | };
94 |
95 | // 打印连接信息
96 | console.error(`[Init] Found database configuration in environment variables: ${this.config.host}:${this.config.port}/${this.config.database}`);
97 | }
98 |
99 | this.setupToolHandlers();
100 |
101 | // Error handling
102 | this.server.onerror = (error) => console.error('[MCP Error]', error);
103 | process.on('SIGINT', async () => {
104 | await this.cleanup();
105 | process.exit(0);
106 | });
107 | }
108 |
109 | private async cleanup() {
110 | if (this.pool) {
111 | try {
112 | console.error(`[Cleanup] Closing connection pool, active connections: ${this.activeConnections}`);
113 | await this.pool.end();
114 | console.error('[Cleanup] Connection pool closed successfully');
115 | } catch (error) {
116 | console.error(`[Cleanup] Error closing pool: ${getErrorMessage(error)}`);
117 | }
118 | this.pool = null;
119 | }
120 | await this.server.close();
121 | }
122 |
123 | private async ensureConnection() {
124 | // 如果已经有一个正在进行的连接操作,直接返回那个Promise
125 | if (this.connectionPromise) {
126 | return this.connectionPromise;
127 | }
128 |
129 | // 如果没有配置,抛出错误
130 | if (!this.config) {
131 | throw new McpError(
132 | ErrorCode.InvalidRequest,
133 | 'Database configuration not set. Use connect_db tool first or set environment variables.'
134 | );
135 | }
136 |
137 | // 创建连接池
138 | if (!this.pool) {
139 | const connectionPromise = (async () => {
140 | try {
141 | // 创建连接池而不是单一连接,提高性能和稳定性
142 | this.pool = mysql.createPool({
143 | ...this.config,
144 | waitForConnections: true,
145 | connectionLimit: this.maxConnections, // 增加连接池大小以支持更多同时连接
146 | queueLimit: 0
147 | });
148 |
149 | // 测试连接池是否正常工作
150 | const conn = await this.pool.getConnection();
151 | await conn.ping();
152 | conn.release();
153 |
154 | // 这里一定有config,因为前面已经检查过
155 | const config = this.config!;
156 | console.error(`Successfully connected to MySQL database: ${config.host}:${config.port || 3306}/${config.database}`);
157 | this.isConnected = true;
158 |
159 | // 连接成功后清空connectionPromise,允许将来的连接检查创建新的Promise
160 | this.connectionPromise = null;
161 | } catch (error) {
162 | this.pool = null; // 重置连接池对象以便下次重试
163 | this.isConnected = false;
164 | this.connectionPromise = null; // 重置连接Promise
165 |
166 | const errorMsg = getErrorMessage(error);
167 | console.error(`Database connection failed: ${errorMsg}`);
168 | throw new McpError(
169 | ErrorCode.InternalError,
170 | `Failed to connect to database: ${errorMsg}`
171 | );
172 | }
173 | })();
174 |
175 | this.connectionPromise = connectionPromise;
176 | return connectionPromise;
177 | }
178 |
179 | return Promise.resolve();
180 | }
181 |
182 | private setupToolHandlers() {
183 | this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
184 | tools: [
185 | {
186 | name: 'connect_db',
187 | description: 'Connect to MySQL database (optional if environment variables are set)',
188 | inputSchema: {
189 | type: 'object',
190 | properties: {
191 | host: {
192 | type: 'string',
193 | description: 'Database host',
194 | },
195 | user: {
196 | type: 'string',
197 | description: 'Database user',
198 | },
199 | password: {
200 | type: 'string',
201 | description: 'Database password',
202 | },
203 | database: {
204 | type: 'string',
205 | description: 'Database name',
206 | },
207 | port: {
208 | type: 'number',
209 | description: 'Database port (optional)',
210 | },
211 | },
212 | required: ['host', 'user', 'password', 'database'],
213 | },
214 | },
215 | {
216 | name: 'query',
217 | description: 'Execute a SELECT query',
218 | inputSchema: {
219 | type: 'object',
220 | properties: {
221 | sql: {
222 | type: 'string',
223 | description: 'SQL SELECT query',
224 | },
225 | params: {
226 | type: 'array',
227 | items: {
228 | type: ['string', 'number', 'boolean', 'null'],
229 | },
230 | description: 'Query parameters (optional)',
231 | },
232 | },
233 | required: ['sql'],
234 | },
235 | },
236 | {
237 | name: 'list_tables',
238 | description: 'List all tables in the database',
239 | inputSchema: {
240 | type: 'object',
241 | properties: {
242 | random_string: {
243 | type: 'string',
244 | description: 'Dummy parameter for no-parameter tools',
245 | }
246 | },
247 | required: [], // 修改为可选参数
248 | },
249 | },
250 | {
251 | name: 'describe_table',
252 | description: 'Get table structure',
253 | inputSchema: {
254 | type: 'object',
255 | properties: {
256 | table: {
257 | type: 'string',
258 | description: 'Table name',
259 | },
260 | },
261 | required: ['table'],
262 | },
263 | },
264 | ],
265 | }));
266 |
267 | this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
268 | // 获取请求ID用于日志
269 | const requestId = `req-${Date.now()}-${Math.floor(Math.random() * 1000)}`;
270 | console.error(`[${requestId}] Received tool call: ${request.params.name}`);
271 |
272 | // 增加活跃连接计数
273 | this.activeConnections++;
274 | console.error(`[${requestId}] Active connections: ${this.activeConnections}`);
275 |
276 | try {
277 | // 如果有环境变量配置并且不是connect_db命令,先确保连接存在
278 | if (this.config && !this.isConnected && request.params.name !== 'connect_db') {
279 | try {
280 | console.error(`[${requestId}] Auto-connecting to database`);
281 | await this.ensureConnection();
282 | } catch (error) {
283 | console.error(`[${requestId}] Auto-connection failed: ${getErrorMessage(error)}`);
284 | // 不抛出错误,让后续操作根据实际情况处理
285 | }
286 | }
287 |
288 | let result;
289 | switch (request.params.name) {
290 | case 'connect_db':
291 | result = await this.handleConnectDb(requestId, request.params.arguments);
292 | break;
293 | case 'query':
294 | result = await this.handleQuery(requestId, request.params.arguments);
295 | break;
296 | case 'list_tables':
297 | result = await this.handleListTables(requestId);
298 | break;
299 | case 'describe_table':
300 | result = await this.handleDescribeTable(requestId, request.params.arguments);
301 | break;
302 | default:
303 | throw new McpError(
304 | ErrorCode.MethodNotFound,
305 | `Unknown tool: ${request.params.name}`
306 | );
307 | }
308 |
309 | return result;
310 | } finally {
311 | // 减少活跃连接计数
312 | this.activeConnections--;
313 | console.error(`[${requestId}] Request completed, active connections: ${this.activeConnections}`);
314 | }
315 | });
316 | }
317 |
318 | private async handleConnectDb(requestId: string, args: any) {
319 | // 验证参数
320 | if (!args.host || !args.user || args.password === undefined || args.password === null || !args.database) {
321 | throw new McpError(
322 | ErrorCode.InvalidParams,
323 | 'Missing required database configuration parameters'
324 | );
325 | }
326 |
327 | // 关闭现有连接池
328 | if (this.pool) {
329 | try {
330 | console.error(`[${requestId}] Closing existing connection pool`);
331 | await this.pool.end();
332 | } catch (error) {
333 | console.error(`[${requestId}] Error closing pool: ${getErrorMessage(error)}`);
334 | }
335 | this.pool = null;
336 | }
337 |
338 | this.config = {
339 | host: args.host,
340 | user: args.user,
341 | password: args.password,
342 | database: args.database,
343 | port: args.port || 3306, // 确保有默认端口
344 | };
345 |
346 | try {
347 | console.error(`[${requestId}] Connecting to database: ${this.config.host}:${this.config.port}/${this.config.database}`);
348 | await this.ensureConnection();
349 | return {
350 | content: [
351 | {
352 | type: 'text',
353 | text: 'Successfully connected to database',
354 | },
355 | ],
356 | };
357 | } catch (error) {
358 | throw new McpError(
359 | ErrorCode.InternalError,
360 | `Failed to connect to database: ${getErrorMessage(error)}`
361 | );
362 | }
363 | }
364 |
365 | private async handleQuery(requestId: string, args: any) {
366 | await this.ensureConnection();
367 |
368 | if (!args.sql) {
369 | throw new McpError(ErrorCode.InvalidParams, 'SQL query is required');
370 | }
371 |
372 | if (!args.sql.trim().toUpperCase().startsWith('SELECT')) {
373 | throw new McpError(
374 | ErrorCode.InvalidParams,
375 | 'Only SELECT queries are allowed with query tool'
376 | );
377 | }
378 |
379 | const sql = args.sql.trim();
380 |
381 | // 验证SQL安全性
382 | if (!validateSqlQuery(sql)) {
383 | throw new McpError(
384 | ErrorCode.InvalidParams,
385 | 'SQL query contains potentially dangerous patterns'
386 | );
387 | }
388 |
389 | try {
390 | console.error(`[${requestId}] Executing query: ${sql.substring(0, 100)}${sql.length > 100 ? '...' : ''}`);
391 | const [rows] = await this.pool!.query(args.sql, args.params || []);
392 |
393 | // 计算结果集大小
394 | const resultSize = JSON.stringify(rows).length;
395 | console.error(`[${requestId}] Query executed successfully, result size: ${resultSize} bytes`);
396 |
397 | return {
398 | content: [
399 | {
400 | type: 'text',
401 | text: JSON.stringify(rows, null, 2),
402 | },
403 | ],
404 | };
405 | } catch (error) {
406 | const errorMsg = getErrorMessage(error);
407 | console.error(`[${requestId}] Query execution failed: ${errorMsg}`);
408 | throw new McpError(
409 | ErrorCode.InternalError,
410 | `Query execution failed: ${errorMsg}`
411 | );
412 | }
413 | }
414 |
415 | private async handleListTables(requestId: string) {
416 | await this.ensureConnection();
417 |
418 | try {
419 | console.error(`[${requestId}] Executing SHOW TABLES`);
420 | const [rows] = await this.pool!.query('SHOW TABLES');
421 | console.error(`[${requestId}] SHOW TABLES completed, found ${Array.isArray(rows) ? rows.length : 0} tables`);
422 |
423 | return {
424 | content: [
425 | {
426 | type: 'text',
427 | text: JSON.stringify(rows, null, 2),
428 | },
429 | ],
430 | };
431 | } catch (error) {
432 | const errorMsg = getErrorMessage(error);
433 | console.error(`[${requestId}] Failed to list tables: ${errorMsg}`);
434 | throw new McpError(
435 | ErrorCode.InternalError,
436 | `Failed to list tables: ${errorMsg}`
437 | );
438 | }
439 | }
440 |
441 | private async handleDescribeTable(requestId: string, args: any) {
442 | await this.ensureConnection();
443 |
444 | if (!args.table) {
445 | throw new McpError(ErrorCode.InvalidParams, 'Table name is required');
446 | }
447 |
448 | try {
449 | console.error(`[${requestId}] Executing DESCRIBE ${args.table}`);
450 | const [rows] = await this.pool!.query('DESCRIBE ??', [args.table]);
451 | console.error(`[${requestId}] DESCRIBE completed, found ${Array.isArray(rows) ? rows.length : 0} columns`);
452 |
453 | return {
454 | content: [
455 | {
456 | type: 'text',
457 | text: JSON.stringify(rows, null, 2),
458 | },
459 | ],
460 | };
461 | } catch (error) {
462 | const errorMsg = getErrorMessage(error);
463 | console.error(`[${requestId}] Failed to describe table: ${errorMsg}`);
464 | throw new McpError(
465 | ErrorCode.InternalError,
466 | `Failed to describe table: ${errorMsg}`
467 | );
468 | }
469 | }
470 |
471 | async run() {
472 | const transport = new StdioServerTransport();
473 | await this.server.connect(transport);
474 | console.error('MySQL MCP server running on stdio');
475 | console.error(`Max concurrent connections: ${this.maxConnections}`);
476 |
477 | // 如果配置了环境变量,尝试初始连接
478 | if (this.config && !this.isConnected) {
479 | try {
480 | console.error('[Init] Auto-connecting to database with environment variables');
481 | await this.ensureConnection();
482 | console.error('[Init] Auto-connection succeeded');
483 | } catch (error) {
484 | console.error(`[Init] Auto-connection failed: ${getErrorMessage(error)}`);
485 | // 不抛出错误,让后续操作根据实际情况处理
486 | }
487 | }
488 | }
489 | }
490 |
491 | const server = new MySQLServer();
492 | server.run().catch(console.error);
493 |
```