让 AI IDE 直接读取你的 MySQL 数据库
一款基于 Model Context Protocol (MCP) 的 MySQL 数据库连接工具,支持 Kiro、Cursor 等 AI IDE 直接查询数据库结构、查询具体数据、分析执行计划等操作。
这是一款对数据库的 只读 插件,只允许指定读取数据库的DQL语句。避免AI对数据库造成不可预见的操作导致损失。
快速开始
跟着下面 4 步配置,就可以让 AI IDE 连上 MySQL:
1. 安装 Node.js
(如果本机已经有nodejs请跳过这一步)
- 打开 Node.js 官网下载页面:https://nodejs.org/
- 下载 LTS(长期支持)版本 的 Windows 安装包(
.msi) - 双击安装包,按默认选项一路点击 Next 完成安装
- 安装完成后,打开 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 数据库。
核心流程:
- 启动时从环境变量读取数据库配置,初始化连接池
- 注册 8 个工具的处理器(
ListToolsRequestSchema和CallToolRequestSchema) - 收到请求后,根据工具名路由到对应的处理方法
- 执行 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_HOST | 否 | localhost | 数据库主机地址 |
MYSQL_PORT | 否 | 3306 | 数据库端口 |
MYSQL_USER | 是 | - | 数据库用户名 |
MYSQL_PASSWORD | 是 | - | 数据库密码 |
MYSQL_DATABASE | 是 | - | 默认数据库名 |
MYSQL_CACHE_DIR | 否 | ./cache | 缓存目录,设为 false 禁用缓存 |
MYSQL_QUERY_TIMEOUT | 否 | 30000 | 查询超时时间(毫秒) |
可能遇到的问题
配置后 IDE 提示连接失败?
检查项:
args中的路径是否为绝对路径- 环境变量是否包含
MYSQL_USER、MYSQL_PASSWORD、MYSQL_DATABASE - Node.js 是否已正确安装(
node --version验证)
查大数据时卡住?
可以通过 MYSQL_QUERY_TIMEOUT 调大超时时间,或者优化 SQL 减少返回数据量。
不想用缓存怎么办?
将 MYSQL_CACHE_DIR 设置为 false 或 disabled 即可。
用到的东西
- Model Context Protocol (MCP) - AI 和工具通信的协议
- mysql2 - Node.js 的 MySQL 客户端
- Vitest + fast-check - 测试框架
写在最后
这个工具主要是为了解决一个实际问题:让 AI 帮忙写 SQL 时,不再需要手动复制表结构给 AI,可以直接在对话中查询。如果有问题或建议(尤其是发现有逃过过滤的语句!),欢迎在 Gitee 上提交 Issue。
项目地址:https://gitee.com/mosfield/mysql-mcp-server-nodejs-readonly



Comments NOTHING