Skip to main content
Glama

MySQL MCP Server

by QiPanTanYi
mcp-standalone.ts13 kB
#!/usr/bin/env node /** * MCP Server 独立启动入口 * 通过 stdio 通信,可被 Kiro、Claude Desktop 等 MCP 客户端调用 */ import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js'; import { createConnection, Connection } from 'typeorm'; import * as dotenv from 'dotenv'; import * as fs from 'fs'; import * as path from 'path'; // 加载环境变量 dotenv.config(); // 权限配置 interface PermissionConfig { permissions: { allowedTables: string[]; readOnlyTables: string[]; allowedOperations: Record<string, string[]>; forbiddenOperations: string[]; maxQueryLimit: number; }; audit: { enabled: boolean; logPath: string; trackChanges: boolean; }; } let permissionConfig: PermissionConfig; let dbConnection: Connection; // 加载权限配置 function loadPermissions(): PermissionConfig { const configPath = path.join(process.cwd(), 'database-permissions.json'); try { const configData = fs.readFileSync(configPath, 'utf8'); return JSON.parse(configData); } catch (error) { console.error('Failed to load permissions config, using defaults'); return { permissions: { allowedTables: ['users'], readOnlyTables: [], allowedOperations: { users: ['SELECT', 'INSERT', 'UPDATE', 'DELETE'], }, forbiddenOperations: ['DROP TABLE', 'DROP DATABASE', 'TRUNCATE', 'ALTER TABLE'], maxQueryLimit: 1000, }, audit: { enabled: true, logPath: './logs/audit.log', trackChanges: true, }, }; } } // 审计日志 function logAudit(log: any) { if (!permissionConfig.audit.enabled) return; const logDir = path.join(process.cwd(), 'logs'); if (!fs.existsSync(logDir)) { fs.mkdirSync(logDir, { recursive: true }); } const logPath = path.join(process.cwd(), permissionConfig.audit.logPath); const logEntry = { ...log, timestamp: new Date().toISOString(), }; fs.appendFileSync(logPath, JSON.stringify(logEntry) + '\n', 'utf8'); } // 权限验证 function canAccessTable(tableName: string): boolean { const allTables = [ ...permissionConfig.permissions.allowedTables, ...permissionConfig.permissions.readOnlyTables, ]; return allTables.includes(tableName); } function canPerformOperation(tableName: string, operation: string): boolean { if (!canAccessTable(tableName)) return false; const allowedOps = permissionConfig.permissions.allowedOperations[tableName] || []; return allowedOps.includes(operation); } function validateSql(sql: string): { valid: boolean; error?: string } { const upperSql = sql.toUpperCase(); for (const forbidden of permissionConfig.permissions.forbiddenOperations) { if (upperSql.includes(forbidden.toUpperCase())) { return { valid: false, error: `禁止的操作: ${forbidden}` }; } } return { valid: true }; } // 数据库操作 async function listTables(): Promise<string[]> { const result = await dbConnection.query('SHOW TABLES'); const dbName = process.env.DB_NAME || 'mcp-test'; return result.map((row: any) => row[`Tables_in_${dbName}`]); } async function getTableSchema(tableName: string): Promise<any> { if (!canAccessTable(tableName)) { throw new Error(`无权访问表 '${tableName}'`); } const columns = await dbConnection.query(`DESCRIBE ${tableName}`); return { tableName, columns: columns.map((col: any) => ({ field: col.Field, type: col.Type, null: col.Null, key: col.Key, default: col.Default, extra: col.Extra, })), }; } async function queryDatabase(sql: string, params: any[] = []): Promise<any> { const validation = validateSql(sql); if (!validation.valid) { throw new Error(validation.error); } const maxLimit = permissionConfig.permissions.maxQueryLimit; if (sql.toUpperCase().includes('SELECT') && !sql.toUpperCase().includes('LIMIT')) { sql += ` LIMIT ${maxLimit}`; } const result = await dbConnection.query(sql, params); logAudit({ operation: 'SELECT', sql, params, rowCount: result.length, success: true, }); return result; } async function insertRecord(tableName: string, data: Record<string, any>): Promise<any> { if (!canAccessTable(tableName)) { throw new Error(`无权访问表 '${tableName}'`); } if (!canPerformOperation(tableName, 'INSERT')) { throw new Error(`表 '${tableName}' 不允许 INSERT 操作`); } const keys = Object.keys(data); const values = Object.values(data); const placeholders = keys.map(() => '?').join(', '); const sql = `INSERT INTO ${tableName} (${keys.join(', ')}) VALUES (${placeholders})`; const result = await dbConnection.query(sql, values); logAudit({ operation: 'INSERT', tableName, data, insertId: result.insertId, success: true, }); return { insertId: result.insertId, affectedRows: result.affectedRows, insertedData: data, }; } async function updateRecord( tableName: string, data: Record<string, any>, where: Record<string, any> ): Promise<any> { if (!canAccessTable(tableName)) { throw new Error(`无权访问表 '${tableName}'`); } if (!canPerformOperation(tableName, 'UPDATE')) { throw new Error(`表 '${tableName}' 不允许 UPDATE 操作`); } // 获取修改前的数据 let beforeData = null; if (permissionConfig.audit.trackChanges) { const whereClause = Object.keys(where) .map((key) => `${key} = ?`) .join(' AND '); const selectSql = `SELECT * FROM ${tableName} WHERE ${whereClause}`; beforeData = await dbConnection.query(selectSql, Object.values(where)); } const setClause = Object.keys(data) .map((key) => `${key} = ?`) .join(', '); const whereClause = Object.keys(where) .map((key) => `${key} = ?`) .join(' AND '); const sql = `UPDATE ${tableName} SET ${setClause} WHERE ${whereClause}`; const params = [...Object.values(data), ...Object.values(where)]; const result = await dbConnection.query(sql, params); logAudit({ operation: 'UPDATE', tableName, beforeData, afterData: data, affectedRows: result.affectedRows, success: true, }); return { affectedRows: result.affectedRows, changedRows: result.changedRows, beforeData, afterData: data, }; } async function deleteRecord(tableName: string, where: Record<string, any>): Promise<any> { if (!canAccessTable(tableName)) { throw new Error(`无权访问表 '${tableName}'`); } if (!canPerformOperation(tableName, 'DELETE')) { throw new Error(`表 '${tableName}' 不允许 DELETE 操作`); } // 获取删除前的数据 let beforeData = null; if (permissionConfig.audit.trackChanges) { const whereClause = Object.keys(where) .map((key) => `${key} = ?`) .join(' AND '); const selectSql = `SELECT * FROM ${tableName} WHERE ${whereClause}`; beforeData = await dbConnection.query(selectSql, Object.values(where)); } const whereClause = Object.keys(where) .map((key) => `${key} = ?`) .join(' AND '); const sql = `DELETE FROM ${tableName} WHERE ${whereClause}`; const params = Object.values(where); const result = await dbConnection.query(sql, params); logAudit({ operation: 'DELETE', tableName, beforeData, affectedRows: result.affectedRows, success: true, }); return { affectedRows: result.affectedRows, deletedData: beforeData, }; } // 主函数 async function main() { // 加载配置 permissionConfig = loadPermissions(); // 连接数据库 dbConnection = await createConnection({ type: 'mysql', host: process.env.DB_HOST || 'localhost', port: parseInt(process.env.DB_PORT || '3306'), username: process.env.DB_USER || 'root', password: process.env.DB_PASSWORD || '', database: process.env.DB_NAME || 'mcp-test', logging: false, }); console.error('✅ Database connected'); // 创建 MCP Server const server = new Server( { name: 'mysql-mcp-server', version: '1.0.0', }, { capabilities: { tools: {}, }, } ); // 注册工具列表 server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [ { name: 'list_tables', description: '列出数据库中的所有表', inputSchema: { type: 'object', properties: {}, }, }, { name: 'get_schema', description: '获取指定表的结构信息', inputSchema: { type: 'object', properties: { tableName: { type: 'string', description: '表名' }, }, required: ['tableName'], }, }, { name: 'query_database', description: '执行 SELECT 查询', inputSchema: { type: 'object', properties: { sql: { type: 'string', description: 'SQL 查询语句' }, params: { type: 'array', description: '查询参数(可选)', items: { type: 'string' }, }, }, required: ['sql'], }, }, { name: 'insert_record', description: '向表中插入一条记录', inputSchema: { type: 'object', properties: { tableName: { type: 'string', description: '表名' }, data: { type: 'object', description: '要插入的数据(键值对)' }, }, required: ['tableName', 'data'], }, }, { name: 'update_record', description: '更新表中的记录', inputSchema: { type: 'object', properties: { tableName: { type: 'string', description: '表名' }, data: { type: 'object', description: '要更新的数据(键值对)' }, where: { type: 'object', description: '更新条件(键值对)' }, }, required: ['tableName', 'data', 'where'], }, }, { name: 'delete_record', description: '删除表中的记录', inputSchema: { type: 'object', properties: { tableName: { type: 'string', description: '表名' }, where: { type: 'object', description: '删除条件(键值对)' }, }, required: ['tableName', 'where'], }, }, ], })); // 处理工具调用 server.setRequestHandler(CallToolRequestSchema, async (request) => { try { const { name, arguments: args } = request.params; let result: any; switch (name) { case 'list_tables': result = await listTables(); return { content: [{ type: 'text', text: JSON.stringify({ tables: result }, null, 2) }], }; case 'get_schema': result = await getTableSchema((args as any).tableName); return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], }; case 'query_database': result = await queryDatabase((args as any).sql, (args as any).params || []); return { content: [ { type: 'text', text: JSON.stringify({ rows: result, count: result.length }, null, 2), }, ], }; case 'insert_record': result = await insertRecord((args as any).tableName, (args as any).data); return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], }; case 'update_record': result = await updateRecord( (args as any).tableName, (args as any).data, (args as any).where ); return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], }; case 'delete_record': result = await deleteRecord((args as any).tableName, (args as any).where); return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], }; default: throw new Error(`Unknown tool: ${name}`); } } catch (error: any) { logAudit({ operation: request.params.name, error: error.message, success: false, }); return { content: [{ type: 'text', text: JSON.stringify({ error: error.message }, null, 2) }], isError: true, }; } }); // 启动 stdio transport const transport = new StdioServerTransport(); await server.connect(transport); console.error('🔧 MCP Server started via stdio'); } main().catch((error) => { console.error('Failed to start MCP server:', error); process.exit(1); });

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/QiPanTanYi/mysql-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server