让 AI IDE 直接读取你的 MySQL 数据库

一款基于 Model Context Protocol (MCP) 的 MySQL 数据库连接工具,支持 Kiro、Cursor 等 AI IDE 直接查询数据库结构、查询具体数据、分析执行计划等操作。

这是一款对数据库的 只读 插件,只允许指定读取数据库的DQL语句。避免AI对数据库造成不可预见的操作导致损失。


快速开始

跟着下面 4 步配置,就可以让 AI IDE 连上 MySQL:

1. 安装 Node.js

(如果本机已经有nodejs请跳过这一步)

  1. 打开 Node.js 官网下载页面:https://nodejs.org/
  2. 下载 LTS(长期支持)版本 的 Windows 安装包(.msi
  3. 双击安装包,按默认选项一路点击 Next 完成安装
  4. 安装完成后,打开 cmd(命令提示符)验证安装:
node --version
npm --version

如果正常显示版本号(如 v24.13.0 和 11.6.2),说明安装成功。

2. 从gitee下载项目

git clone https://gitee.com/mosfield/mysql-mcp-server-nodejs-readonly
cd mysql-mcp-server-nodejs
npm install

3. 在IDE配置 MCP服务

在你的 IDE(Kiro/Cursor)的 mcp.json 中添加以下配置:

{
  "mcpServers": {
    "mysql": {
      "command": "node",
      "args": [
        "C:/你的实际路径/mysql-mcp-server-nodejs/index.js"
      ],
      "env": {
        "MYSQL_HOST": "localhost",
        "MYSQL_PORT": "3306",
        "MYSQL_USER": "root",
        "MYSQL_PASSWORD": "你的密码",
        "MYSQL_DATABASE": "你的数据库名",
        "MYSQL_CACHE_DIR": "C:/你的实际路径/mysql-mcp-server-nodejs/cache",
        "MYSQL_QUERY_TIMEOUT": "30000"
      }
    }
  }
}

4. 重启 IDE,开始对话

配置完成后重启 IDE,然后可以直接问 AI:

  • "查询 users 表的表结构"
  • "列出数据库中的所有表"
  • "分析一下这条 SQL 的执行计划:SELECT * FROM orders WHERE user_id = 100"

项目地址


能做什么

这个工具一共提供了 8 个功能,都是平时查数据库常用的:

工具名功能描述
mysql_get_table_structure获取表的完整结构(字段、类型、约束、索引、注释)
mysql_list_tables列出数据库中的所有表
mysql_execute_query执行 SQL 查询(仅支持 SELECT,结果超 200 行自动缓存)
mysql_get_table_indexes获取表的索引信息
mysql_get_table_constraints获取表的约束信息(主键、外键、唯一约束)
mysql_get_source_code获取存储过程、函数、触发器的源代码
mysql_list_source_objects列出所有存储过程、函数、触发器
mysql_get_explain_plan获取 SQL 执行计划(EXPLAIN FORMAT=JSON)

项目结构

mysql-mcp-server-nodejs/
├── index.js              # 主程序入口,包含 MCP Server 实现
├── package.json          # 项目配置和依赖
├── cache/                # 缓存目录(自动生成)
└── __tests__/            # 测试目录
    ├── property/         # 属性测试(基于 fast-check)
    └── unit/             # 单元测试

实现思路

1. 整体架构

这个工具本质上是一个基于 MCP 协议的命令行程序。IDE 通过 stdio 与它通信,它再把请求转发给 MySQL 数据库。

核心流程:

  1. 启动时从环境变量读取数据库配置,初始化连接池
  2. 注册 8 个工具的处理器(ListToolsRequestSchema 和 CallToolRequestSchema
  3. 收到请求后,根据工具名路由到对应的处理方法
  4. 执行 SQL,格式化结果,返回给 IDE

主程序结构:

class MySQLMCPServer {
  constructor() {
    // 创建 MCP 服务器实例
    this.server = new Server(
      { name: 'mysql-mcp-server', version: '1.0.0' },
      { capabilities: { tools: {} } }
    );
    
    // 设置工具处理器
    this.setupToolHandlers();
    
    // 清理旧缓存
    this.cleanOldCache();
    
    // 错误处理和优雅关闭
    this.server.onerror = (error) => console.error('[MCP Error]', error);
    process.on('SIGINT', async () => {
      await this.shutdown();
      process.exit(0);
    });
  }
  
  setupToolHandlers() {
    // 注册 ListToolsRequestSchema - 返回可用工具列表
    this.server.setRequestHandler(ListToolsRequestSchema, async () => {
      return { tools: [/* 8 个工具的定义 */] };
    });
    
    // 注册 CallToolRequestSchema - 处理工具调用
    this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
      const { name, arguments: args } = request.params;
      
      switch (name) {
        case 'mysql_get_table_structure':
          return await this.getTableStructure(args.table_name, args.database);
        case 'mysql_execute_query':
          return await this.executeQuery(args.sql, args.params);
        // ... 其他工具
      }
    });
  }
}

// 启动服务器
const server = new MySQLMCPServer();
server.run().catch(console.error);

2. 只读查询的实现

安全是最先考虑的。实现方式是在执行 SQL 前加一层校验:

validateReadOnlySQL(sql) {
  const trimmedSql = sql.trim().toUpperCase();
  
  // 必须以 SELECT 或 WITH (CTE) 开头
  if (!trimmedSql.startsWith('SELECT') && !trimmedSql.startsWith('WITH')) {
    throw new Error('Only SELECT/WITH queries are allowed');
  }
  
  // 移除字符串和注释后再检查,防止绕过
  let sanitized = trimmedSql
    .replace(/'[^']*'/g, '')           // 移除单引号字符串
    .replace(/\/\*[\s\S]*?\*\//g, '')  // 移除多行注释
    .replace(/--[^\n]*/g, '');         // 移除行注释
  
  // 检查危险关键字
  const forbiddenPatterns = [
    /\bINSERT\b/, /\bUPDATE\b/, /\bDELETE\b/, /\bDROP\b/,
    /\bCREATE\b/, /\bALTER\b/, /\bTRUNCATE\b/, /\bEXECUTE\b/,
    /\bCALL\b/, /\bCOMMIT\b/, /\bROLLBACK\b/
  ];
  
  for (const pattern of forbiddenPatterns) {
    if (pattern.test(sanitized)) {
      throw new Error(`Forbidden SQL keyword detected`);
    }
  }
}

这里有个细节:必须先移除 SQL 中的字符串和注释,再检查关键字。否则有人写 SELECT 'DELETE' FROM ... 就会被误杀,或者通过注释隐藏危险操作。

3. 表结构的获取

表结构信息来自 information_schema,需要联合查询多个表:

async getTableStructure(tableName, database = null) {
  const db = database || MYSQL_DATABASE;
  let connection;
  try {
    connection = await this.getConnection();

    // 查询列信息
    const [columns] = await connection.execute(
      `SELECT COLUMN_NAME, DATA_TYPE, CHARACTER_MAXIMUM_LENGTH, NUMERIC_PRECISION, 
              NUMERIC_SCALE, IS_NULLABLE, COLUMN_DEFAULT, COLUMN_COMMENT
       FROM information_schema.COLUMNS 
       WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ?
       ORDER BY ORDINAL_POSITION`,
      [db, tableName]
    );

    // 查询主键
    const [pkRows] = await connection.execute(
      `SELECT kcu.COLUMN_NAME
       FROM information_schema.KEY_COLUMN_USAGE kcu
       JOIN information_schema.TABLE_CONSTRAINTS tc 
         ON kcu.CONSTRAINT_NAME = tc.CONSTRAINT_NAME 
         AND kcu.TABLE_SCHEMA = tc.TABLE_SCHEMA
         AND kcu.TABLE_NAME = tc.TABLE_NAME
       WHERE tc.CONSTRAINT_TYPE = 'PRIMARY KEY'
         AND tc.TABLE_SCHEMA = ? AND tc.TABLE_NAME = ?`,
      [db, tableName]
    );

    const primaryKeys = pkRows.map(row => row.COLUMN_NAME);
    
    const columnList = columns.map(row => ({
      name: row.COLUMN_NAME,
      data_type: row.DATA_TYPE,
      length: row.CHARACTER_MAXIMUM_LENGTH,
      precision: row.NUMERIC_PRECISION,
      scale: row.NUMERIC_SCALE,
      nullable: row.IS_NULLABLE === 'YES',
      default_value: row.COLUMN_DEFAULT,
      is_primary_key: primaryKeys.includes(row.COLUMN_NAME),
      comment: row.COLUMN_COMMENT || null,
    }));

    return {
      content: [{
        type: 'text',
        text: JSON.stringify({ 
          table_name: tableName, 
          database: db, 
          columns: columnList, 
          primary_keys: primaryKeys 
        }, null, 2),
      }],
    };
  } finally {
    if (connection) connection.release();
  }
}

4. 缓存机制

缓存有两个用途:一是避免大数据量导致 IDE 卡顿,二是减少重复查询。

查询结果的缓存:

async executeQuery(sql, params = {}) {
  let connection;
  try {
    // SQL 安全检查
    this.validateReadOnlySQL(sql);
    
    connection = await this.getConnection();
    
    // 执行查询
    const paramValues = Array.isArray(params) ? params : Object.values(params || {});
    const [rows, fields] = await connection.execute(
      { sql, timeout: QUERY_TIMEOUT_MS },
      paramValues.length > 0 ? paramValues : undefined
    );
    
    const columns = fields.map(f => f.name);
    const jsonResult = JSON.stringify({ columns, rows }, null, 2);
    
    // 超过 200 行的结果写入缓存文件
    if (CACHE_ENABLED && rows.length > CACHE_LINE_THRESHOLD) {
      this.cleanOldCache();
      const hash = crypto.createHash('md5')
        .update(sql + JSON.stringify(params))
        .digest('hex').slice(0, 12);
      const cacheFileName = `query_${hash}.json`;
      const cachedFilePath = this.writeCacheFile(cacheFileName, jsonResult);
      
      if (cachedFilePath) {
        return {
          content: [{
            type: 'text',
            text: JSON.stringify({
              cached: true,
              file_path: cachedFilePath,
              row_count: rows.length,
              column_count: columns.length,
              message: `查询返回 ${rows.length} 行,结果已缓存到文件`
            }, null, 2),
          }],
        };
      }
    }
    
    // 小结果直接返回
    return {
      content: [{ type: 'text', text: jsonResult }],
    };
  } finally {
    if (connection) connection.release();
  }
}

存储过程源码的缓存更复杂一些,需要判断源码是否被修改过:

async getSourceCode(objectName, objectType, database = null) {
  const db = database || MYSQL_DATABASE;
  const type = objectType.toUpperCase();
  
  const cacheFileName = `${db}.${type}.${objectName}.sql`;
  const cachedFilePath = this.getCachePath(cacheFileName);
  
  const connection = await this.getConnection();
  try {
    // 查询对象的最后修改时间
    const [metaRows] = await connection.execute(
      `SELECT LAST_ALTERED FROM information_schema.ROUTINES 
       WHERE ROUTINE_SCHEMA = ? AND ROUTINE_NAME = ? AND ROUTINE_TYPE = ?`,
      [db, objectName, type]
    );
    const lastAltered = metaRows[0]?.LAST_ALTERED;
    
    // 如果缓存文件的修改时间 >= 对象修改时间,直接返回缓存
    if (cachedFilePath && fs.existsSync(cachedFilePath)) {
      const fileStat = fs.statSync(cachedFilePath);
      if (lastAltered && fileStat.mtime >= new Date(lastAltered)) {
        return {
          content: [{
            type: 'text',
            text: JSON.stringify({
              object_name: objectName,
              object_type: type,
              cached: true,
              cache_hit: true,
              file_path: cachedFilePath,
              message: '缓存命中,对象未变更'
            }, null, 2),
          }],
        };
      }
    }
    
    // 获取源码
    const [rows] = await connection.execute(
      `SHOW CREATE ${type} \`${db}\`.\`${objectName}\``
    );
    const sourceCode = rows[0][`Create ${type}`];
    
    // 缓存大文件
    if (CACHE_ENABLED && sourceCode.split('\n').length > CACHE_LINE_THRESHOLD) {
      const writtenPath = this.writeCacheFile(cacheFileName, sourceCode);
      // ... 返回缓存信息
    }
    
    return {
      content: [{
        type: 'text',
        text: JSON.stringify({
          object_name: objectName,
          object_type: type,
          cached: false,
          source_code: sourceCode
        }, null, 2),
      }],
    };
  } finally {
    connection.release();
  }
}

5. 连接池配置

this.pool = mysql.createPool({
  host: MYSQL_HOST,
  port: MYSQL_PORT,
  user: MYSQL_USER,
  password: MYSQL_PASSWORD,
  database: MYSQL_DATABASE,
  connectionLimit: 5,              // 最多 5 个连接
  connectTimeout: QUERY_TIMEOUT_MS,
  enableMultipleStatements: false, // 禁止多语句,防止注入
  queueLimit: 20,                  // 等待队列上限
  idleTimeout: 60000,              // 空闲连接 60 秒回收
});

enableMultipleStatements: false 很重要,可以防止多语句注入攻击。


举个例子

查表结构

示例:

"帮我看一下 users 表的表结构"

返回结果:

{
  "table_name": "users",
  "database": "mydb",
  "columns": [
    {
      "name": "id",
      "data_type": "bigint",
      "nullable": false,
      "is_primary_key": true,
      "comment": "主键ID"
    },
    {
      "name": "username",
      "data_type": "varchar",
      "length": 50,
      "nullable": false,
      "comment": "用户名"
    }
  ],
  "primary_keys": ["id"]
}

看 SQL 执行计划

示例:

"分析一下这条 SQL 的执行计划:SELECT * FROM orders WHERE user_id = 100"

可以查看 SQL 的执行计划,包括是否使用索引、扫描行数等信息。

看存储过程源码

示例:

"获取 sp_calc_order_total 存储过程的源码"

可以直接查看存储过程、函数或触发器的源代码。


需要什么环境

  • Node.js >= 18(建议用 LTS 版本)
  • MySQL >= 5.7

配置参数一览

环境变量必填默认值说明
MYSQL_HOSTlocalhost数据库主机地址
MYSQL_PORT3306数据库端口
MYSQL_USER-数据库用户名
MYSQL_PASSWORD-数据库密码
MYSQL_DATABASE-默认数据库名
MYSQL_CACHE_DIR./cache缓存目录,设为 false 禁用缓存
MYSQL_QUERY_TIMEOUT30000查询超时时间(毫秒)

可能遇到的问题

配置后 IDE 提示连接失败?

检查项:

  1. args 中的路径是否为绝对路径
  2. 环境变量是否包含 MYSQL_USERMYSQL_PASSWORDMYSQL_DATABASE
  3. Node.js 是否已正确安装(node --version 验证)

查大数据时卡住?

可以通过 MYSQL_QUERY_TIMEOUT 调大超时时间,或者优化 SQL 减少返回数据量。

不想用缓存怎么办?

将 MYSQL_CACHE_DIR 设置为 false 或 disabled 即可。


用到的东西


写在最后

这个工具主要是为了解决一个实际问题:让 AI 帮忙写 SQL 时,不再需要手动复制表结构给 AI,可以直接在对话中查询。如果有问题或建议(尤其是发现有逃过过滤的语句!),欢迎在 Gitee 上提交 Issue。


项目地址https://gitee.com/mosfield/mysql-mcp-server-nodejs-readonly

如人饮水,冷暖自知。
最后更新于 2026-04-16