基于MCP协议构建MySQL MCP Server,让AI编程助手安全操作数据库
1. 项目概述当AI开始“理解”你的数据库最近在折腾AI编程助手特别是Cursor发现一个挺有意思的现象你跟它说“帮我查一下上个月的订单数据”它大概率会给你编一段看起来像模像样的SQL但数据库里可能根本没有orders这个表或者字段名完全对不上。这感觉就像让一个顶尖的厨师去一个完全陌生的厨房做饭他刀工火候再好不知道食材和调料放在哪儿也做不出像样的菜。这个问题的核心就是AI模型LLM与你的私有数据、特定工具之间存在着一道难以逾越的“信息鸿沟”。而MCPModel Context Protocol就是为填平这道鸿沟而生的“桥梁协议”。它不是什么高深莫测的新框架你可以把它理解为一套标准化的“插座”和“插头”规范。你的数据库比如MySQL、你的API、你的内部工具只要按照MCP的标准做成一个“Server”服务器即插头就能被支持MCP的“Client”客户端即插座如Cursor、Claude Desktop识别并调用。于是AI助手不再是一个只会空想的“理论家”它变成了一个能直接操作你数据库、调用你内部API的“实干家”。这个项目要探讨的就是如何亲手搭建这座桥让Cursor这类AI编程助手通过MCP协议真正“理解”并操作你的MySQL数据库。这不是简单的插件安装而是一套让AI融入你现有技术栈工作流的系统工程。我会从为什么需要MCP讲起带你一步步拆解MCP的核心组件手把手实现一个连接MySQL的MCP Server并最终在Cursor中验证效果分享其中踩过的坑和总结出的实战经验。2. MCP协议核心拆解AI的“手”和“眼”要理解MCP如何工作我们得先抛开那些复杂的术语把它想象成给AI安装“手”和“眼”。LLM本身是一个强大的“大脑”它擅长理解和生成语言但它没有“手”去操作数据库也没有“眼”去查看服务器状态。MCP协议的核心就是定义了一套标准方式让“大脑”可以安全、可控地指挥各种各样的“手”和“眼”。2.1 核心组件与通信模型MCP的架构非常清晰主要包含三个角色它们之间的通信基于JSON-RPC over stdio标准输入输出或SSE服务器发送事件这种设计让它极其轻量和通用。MCP Client客户端这是AI能力的消费方。比如Cursor编辑器、Claude Desktop应用或者任何集成了MCP SDK的应用。Client的角色是向用户提供AI交互界面并向MCP Server发起工具调用或内容读取的请求。你可以把它看作“大脑”的对外接口。MCP Server服务器这是能力的提供方。它封装了对特定资源如MySQL数据库、文件系统、天气API的操作逻辑。一个Server可以提供一个或多个“工具”Tools或“资源”Resources。它就像一个个专属的“手”或“眼”。我们本项目要构建的就是一个MySQL Server。MCP Host宿主这是连接Client和Server的“调度中心”或“运行时环境”。它负责启动和管理一个或多个MCP Server并在Client和Server之间路由消息。Claude Desktop、Cursor内置了MCP Host。在开发时我们也会使用官方工具modelcontextprotocol/sdk来模拟Host进行测试。它们之间的关系可以用一个简单的场景来类比你想让AI助手帮你查数据Client发出指令 - MCP Host收到指令知道该找谁找到MySQL Server - MySQL Server执行查询手部动作 - 结果通过Host返回给Client - AI大脑将结果组织成自然语言回复给你。2.2 能力抽象Tools与ResourcesMCP协议将Server能提供的能力抽象为两大类这是理解其功能边界的关键Tools工具代表一个可执行的动作。调用Tools就像让AI“用手做一件事”。每个Tool都有明确的名称、描述、输入参数JSON Schema定义和输出。例如我们的MySQL Server可以提供execute_query: 执行一条SELECT查询语句。list_tables: 列出数据库中的所有表。get_table_schema: 获取指定表的字段结构。 当用户在Cursor里说“列出用户表的前10条数据”CursorClient就会调用MySQL Server的execute_query这个Tool并传入参数sql: SELECT * FROM users LIMIT 10。Resources资源代表可读取的静态或动态内容。读取Resources就像让AI“用眼查看一份资料”。每个Resource有一个唯一的uri如mysql://my_db/users/schema和对应的文本内容。例如我们可以将数据库的表结构定义作为Resource提供mysql://localhost:3306/mydb/tables资源的内容是所有表名的列表。mysql://localhost:3306/mydb/tables/users资源的内容是users表的CREATE TABLE语句。 这样AI在回答问题前可以先“阅读”这些Resource来了解数据库结构从而生成更准确的SQL。注意一个常见的误解是认为MCP Server必须同时提供Tools和Resources。实际上这取决于你的需求。对于数据库操作Tools执行查询是核心而提供Resources表结构则能极大提升AI生成SQL的准确性是强烈推荐的做法。2.3 为什么是Stdio/SSE安全与集成的考量你可能会问为什么用Stdio标准输入输出这种“古老”的方式通信而不是更常见的HTTP API这恰恰是MCP设计的精妙之处。无网络依赖与极致简化Stdio通信发生在同一台机器的进程之间无需处理网络端口、防火墙、HTTPS证书等复杂问题。这使得MCP Server可以像本地命令行工具一样简单部署和运行。安全性由于通信不暴露网络端口外部无法直接访问MCP Server减少了攻击面。权限完全由启动Server的Host环境控制。进程生命周期管理Host可以轻松地启动、停止和监控Server进程。当Client断开连接时Host可以清理所有相关Server进程避免资源泄漏。SSE用于流式响应对于需要长时间运行或流式返回结果的操作例如监控日志MCP支持SSE允许Server逐步返回数据用户体验更好。这种设计让MCP在提供强大扩展能力的同时保持了本地化工具应有的简洁和安全特别适合集成到桌面AI应用中。3. 构建MySQL MCP Server从零到一的实战理论讲完了我们动手建一个。我将使用Node.js和官方SDK来构建因为这是目前最成熟、文档最全的路径。别担心即使你不是Node专家跟着步骤也能走通。3.1 环境准备与项目初始化首先确保你的开发环境已经就绪Node.js版本18或以上。可以去官网下载安装。MySQL本地安装一个MySQL实例5.7或8.0均可并创建一个测试数据库和表。比如CREATE DATABASE mcp_demo; USE mcp_demo; CREATE TABLE users ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL, email VARCHAR(100) NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); INSERT INTO users (username, email) VALUES (alice, aliceexample.com), (bob, bobexample.com);代码编辑器VS Code或你喜欢的任何编辑器。接下来创建项目目录并初始化mkdir mcp-mysql-server cd mcp-mysql-server npm init -y安装核心依赖npm install modelcontextprotocol/sdk mysql2modelcontextprotocol/sdk官方SDK提供了构建Server和Client的所有工具类。mysql2一个性能更好的MySQL客户端库支持Promise。3.2 Server核心逻辑实现我们创建一个server.js文件这是整个Server的核心。第一步引入依赖并建立数据库连接const { Server } require(modelcontextprotocol/sdk/server/index.js); const { StdioServerTransport } require(modelcontextprotocol/sdk/server/stdio.js); const mysql require(mysql2/promise); // 使用Promise接口 // 创建MySQL连接池生产环境建议从环境变量读取配置 const pool mysql.createPool({ host: localhost, user: root, // 替换为你的用户名 password: yourpassword, // 替换为你的密码 database: mcp_demo, waitForConnections: true, connectionLimit: 10, queueLimit: 0 });这里使用连接池而不是单连接是为了避免在高频调用下出现连接数耗尽的问题。连接参数务必通过环境变量如process.env.DB_HOST管理切勿硬编码在代码中。第二步初始化MCP Server并声明能力// 初始化Server const server new Server( { name: mysql-server, version: 0.1.0, }, { capabilities: { tools: {}, // 我们将在这里注册工具 resources: {} // 我们将在这里注册资源 } } ); // 定义工具执行SQL查询 server.setRequestHandler(tools/list, async () { return { tools: [ { name: execute_query, description: Execute a SELECT SQL query against the MySQL database. Use this for reading data., inputSchema: { type: object, properties: { sql: { type: string, description: The SELECT SQL query to execute. } }, required: [sql] } }, { name: list_tables, description: List all tables in the connected database., inputSchema: { type: object, properties: {} } // 无参数 }, { name: get_table_schema, description: Get the CREATE TABLE statement (schema) for a specific table., inputSchema: { type: object, properties: { tableName: { type: string, description: Name of the table. } }, required: [tableName] } } ] }; });这里我们定义了三个工具。注意execute_query的描述中强调了SELECT这是一种安全实践避免AI无意中执行DELETE或DROP语句。在生产环境中你需要更严格的SQL解析和白名单机制。第三步实现工具调用的处理逻辑这是Server的“肌肉”真正执行操作的地方。// 处理工具调用请求 server.setRequestHandler(tools/call, async (request) { const { name, arguments: args } request.params; try { switch (name) { case execute_query: { const { sql } args; // 简单的安全校验只允许SELECT查询可根据需要放宽 if (!sql.trim().toUpperCase().startsWith(SELECT)) { throw new Error(Only SELECT queries are allowed for safety.); } const [rows] await pool.query(sql); return { content: [ { type: text, text: JSON.stringify(rows, null, 2) // 美化输出JSON } ] }; } case list_tables: { const [rows] await pool.query(SHOW TABLES); const tableList rows.map(row Object.values(row)[0]).join(\n); return { content: [{ type: text, text: Tables in database:\n${tableList} }] }; } case get_table_schema: { const { tableName } args; const [rows] await pool.query(SHOW CREATE TABLE ${tableName}); const schema rows[0]?.[Create Table]; return { content: [{ type: text, text: schema || Table ${tableName} not found. }] }; } default: throw new Error(Unknown tool: ${name}); } } catch (error) { // 返回结构化的错误信息帮助AI和用户调试 return { content: [{ type: text, text: Error: ${error.message} }], isError: true }; } });关键点在于错误处理。必须用try...catch包裹并返回格式化的错误信息。直接抛出异常可能导致Server进程崩溃破坏整个MCP会话。第四步实现资源读取可选但推荐为了让AI更好地“理解”数据库结构我们提供资源。// 声明可用的资源 server.setRequestHandler(resources/list, async (request) { const { uri } request.params; // 如果请求了根URI列出所有表资源 if (!uri || uri mysql://schema/) { const [tables] await pool.query(SHOW TABLES); const resources tables.map(table ({ uri: mysql://schema/${Object.values(table)[0]}, name: Schema of table: ${Object.values(table)[0]}, mimeType: text/plain })); // 添加一个总览资源 resources.unshift({ uri: mysql://schema/, name: Database Schema Overview, mimeType: text/plain }); return { resources }; } return { resources: [] }; }); // 处理资源读取请求 server.setRequestHandler(resources/read, async (request) { const { uri } request.params; if (uri mysql://schema/) { const [tables] await pool.query(SHOW TABLES); const tableNames tables.map(row - ${Object.values(row)[0]}).join(\n); return { contents: [{ uri, mimeType: text/plain, text: Available tables:\n${tableNames} }] }; } // 匹配表结构URI如 mysql://schema/users const match uri.match(/^mysql:\/\/schema\/(.)$/); if (match) { const tableName match[1]; const [rows] await pool.query(SHOW CREATE TABLE ??, [tableName]); const schema rows[0]?.[Create Table]; return { contents: [{ uri, mimeType: text/plain, text: schema || // Table ${tableName} not found or inaccessible. }] }; } return { contents: [{ uri, mimeType: text/plain, text: // Resource not found: ${uri} }] }; });这里我们设计了一个简单的资源URI方案mysql://schema/列出所有表mysql://schema/{tableName}获取具体表结构。这种设计让AI能按需浏览数据库元数据。第五步启动Server// 启动Server使用stdio传输 async function main() { const transport new StdioServerTransport(); await server.connect(transport); console.error(MySQL MCP Server running on stdio...); } main().catch((error) { console.error(Server fatal error:, error); process.exit(1); });console.error用于输出日志因为MCP协议使用stdin/stdout进行通信常规的console.log会干扰协议消息。3.3 本地测试与调试在配置Cursor之前强烈建议先本地测试Server是否正常工作。我们可以写一个简单的测试Client脚本test_client.jsconst { Client } require(modelcontextprotocol/sdk/client/index.js); const { StdioClientTransport } require(modelcontextprotocol/sdk/client/stdio.js); const { spawn } require(child_process); async function test() { // 启动Server进程 const serverProcess spawn(node, [server.js]); const transport new StdioClientTransport(serverProcess); const client new Client( { name: test-client, version: 1.0.0 }, { capabilities: {} } ); await client.connect(transport); // 测试列出工具 const tools await client.listTools(); console.log(Available tools:, tools.tools.map(t t.name)); // 测试列出表 const result await client.callTool({ name: list_tables, arguments: {} }); console.log(List tables result:, result.content[0].text); // 测试查询 const queryResult await client.callTool({ name: execute_query, arguments: { sql: SELECT * FROM users LIMIT 1 } }); console.log(Query result:, queryResult.content[0].text); await client.close(); serverProcess.kill(); } test().catch(console.error);运行node test_client.js如果看到工具列表和查询结果恭喜你Server端基本功能已就绪。这个测试步骤能帮你提前发现并解决90%的配置和代码逻辑问题。4. 在Cursor中集成与配置让AI助手“上手”Server准备好了现在要让Cursor这个“大脑”能用上我们造的“手”。Cursor内置了MCP Host支持配置过程直观。4.1 配置Cursor的MCP设置Cursor的配置主要通过一个JSON文件完成。文件位置通常如下macOS:~/Library/Application Support/Cursor/User/globalStorage/mcp.jsonWindows:%APPDATA%\Cursor\User\globalStorage\mcp.jsonLinux:~/.config/Cursor/User/globalStorage/mcp.json如果目录或文件不存在手动创建即可。我们需要编辑这个mcp.json文件将我们的MySQL Server添加进去。{ mcpServers: { mysql-local: { command: node, args: [ /ABSOLUTE/PATH/TO/YOUR/mcp-mysql-server/server.js ], env: { DB_HOST: localhost, DB_USER: root, DB_PASSWORD: yourpassword, DB_DATABASE: mcp_demo } } } }配置详解与避坑指南绝对路径是必须的args里的路径必须是绝对路径。相对路径在Cursor的运行时环境中无法正确解析。这是新手最容易踩的坑。在macOS/Linux上可以用pwd命令获取在Windows上需要完整的盘符路径。环境变量管理敏感信息永远不要在配置文件中硬编码数据库密码。通过env字段传入环境变量然后在server.js中通过process.env.DB_PASSWORD读取。这样更安全也便于在不同环境开发、测试间切换。命令与参数command是你系统里可执行命令的名字如node、python3。args是一个数组第一个元素通常是你的脚本文件绝对路径。如果你的Server是用其他语言如Python、Go写的这里就需要相应调整。多个ServermcpServers对象可以配置多个Server。比如你还可以同时配置一个用于搜索的tavily-mcpServer。Cursor会自动管理它们。4.2 验证与使用对话式数据查询保存mcp.json后完全重启Cursor。这是关键一步因为Cursor只在启动时读取这个配置文件。重启后打开Cursor的聊天界面通常通过Cmd/Ctrl K触发。如果配置成功你应该能在输入框下方或模型选择区域看到类似“可用工具”或“Connected tools”的提示或者至少不会报错。现在进行一场真正的对话测试你“我们数据库里有哪些表”Cursor识别到需要查询数据库自动调用list_tables工具 “根据查询数据库中有以下表users, products, orders。”你“看看users表的结构是什么样的”Cursor调用get_table_schema工具 “users表的结构如下CREATE TABLE users (id int, name varchar(100), ...)”你“帮我查一下最近创建的5个用户按时间倒序排列。”Cursor思考后组合已知的表结构和字段生成SQL并调用execute_query “好的查询语句为SELECT * FROM users ORDER BY created_at DESC LIMIT 5结果如下[...]”这个过程是自动的。Cursor背后的AI模型会根据你的自然语言描述判断意图选择合适的工具并生成正确的调用参数。你不再需要手动编写或粘贴SQLAI真正成为了你和数据库之间的“翻译官”和“操作员”。4.3 高级配置安全性与性能调优基础配置跑通后为了投入实际使用还需要考虑以下几点权限最小化在MySQL中为MCP Server创建一个专用用户只授予它必要的SELECT权限甚至可以通过视图VIEW来限制其可访问的数据范围。绝对不要使用root账户。CREATE USER mcp_clientlocalhost IDENTIFIED BY strong_password; GRANT SELECT ON mcp_demo.* TO mcp_clientlocalhost; -- 或者更细粒度 GRANT SELECT ON mcp_demo.public_view TO mcp_clientlocalhost;SQL注入防护我们的简单示例只做了SELECT前缀检查这是远远不够的。生产环境中应考虑使用参数化查询mysql2库本身支持pool.query(SELECT * FROM users WHERE id ?, [userId])但这对AI动态生成的SQL不直接适用。实现一个简单的SQL解析器或使用sql-parser等库进行语法树白名单校验只允许无副作用的查询语句。限制查询的复杂度如设置最大返回行数LIMIT、禁用多表JOIN或子查询等。连接池与超时确保MySQL连接池配置合理connectionLimit并在Server端为数据库查询设置超时SET SESSION MAX_EXECUTION_TIME10000防止一个慢查询拖死整个Server。日志与监控在Server中添加详细的日志记录工具调用、查询语句、执行时间、错误信息便于后期审计和性能分析。可以将日志输出到文件或标准错误。5. 常见问题、排查与进阶思考即使按照步骤操作也难免会遇到问题。这里记录一些我实践中遇到的典型情况和解决方法。5.1 问题排查清单问题现象可能原因排查步骤Cursor启动后无工具提示或聊天中AI不调用工具。1.mcp.json配置文件路径错误或格式错误。2. Server启动失败如Node路径错误、依赖未安装。3. Cursor未重启。1. 检查mcp.json的JSON语法可用在线校验工具。2. 在终端手动运行配置中的命令如node /path/to/server.js看是否报错。3.务必彻底关闭Cursor并重新打开。AI调用了工具但返回“Error”或超时。1. 数据库连接失败主机、端口、密码错误。2. SQL语句执行错误权限不足、语法错误。3. Server代码逻辑错误或未处理异常。1. 使用test_client.js进行本地测试查看具体错误信息。2. 检查Server代码中的错误处理逻辑确保所有await都有try...catch。3. 查看Cursor的开发者控制台Help - Toggle Developer Tools中的Console日志。工具调用成功但AI不理解结果或胡乱回答。1. 工具返回的数据格式太复杂如嵌套很深的JSON。2. AI的上下文长度有限结果太长被截断。1. 优化工具返回内容尽量简洁、结构化。例如将数据库结果以Markdown表格形式返回比纯JSON更易读。2. 在工具中内置总结或采样逻辑比如只返回前10行并提供总行数。配置多个Server后AI混淆了工具。不同Server提供的工具名称或功能相似。在定义工具时使用更具体、包含领域前缀的名称如mysql_execute_query、postgres_list_tables。在工具描述中也要清晰说明其边界。5.2 性能优化与扩展方向当基本功能稳定后可以考虑以下优化和扩展让这个“AI助手”更强大、更智能提供智能提示资源除了基本的表结构可以创建更丰富的Resources。例如mysql://docs/query_examples: 提供一个文档里面写一些常用的查询示例和业务逻辑说明。mysql://stats/table_row_counts: 动态生成一个资源显示每个表的大致数据量帮助AI决定是否要加LIMIT。这些资源会被AI在思考时自动读取作为背景知识显著提升生成SQL的准确性和合理性。实现更复杂的工具不止于查询可以开发需要逻辑判断的工具。analyze_query_plan: 接收一个查询语句返回其EXPLAIN结果让AI能判断查询性能。suggest_index: 基于慢查询日志或当前查询模式让AI给出索引优化建议虽然最终执行需DBA确认。这些工具将AI从“操作员”提升为“初级分析师”。与其他MCP Server联动MCP的魅力在于组合。你可以同时运行MySQL Server处理数据查询。文件系统Server让AI能读取项目代码文件。Git Server让AI能查看提交历史。Web Search Server如Tavily让AI能联网搜索错误信息。 这样你对AI说“根据最近一周的错误日志文件去数据库里查查关联的用户订单MySQL然后看看官方文档搜索里有没有解决方案”它就能串联起多个工具完成一个复杂的工作流。Server实现的多样性我们的示例是Node.js但MCP协议是语言无关的。社区已经有Python、Go、Rust等多种语言的SDK和示例。你可以根据团队的技术栈选择最合适的语言来实现甚至可以封装现有的脚本或工具为MCP Server极大地降低了集成成本。通过MCP将Cursor与MySQL连接只是一个起点。它展示了一种范式如何将AI大模型强大的语言理解和生成能力与组织内部具体、私有、结构化的工具和数据安全地结合起来。这不仅仅是写一个插件而是在构建一套让AI智能体AI Agent真正落地、融入日常研发工作流的基础设施。当你习惯了用自然语言让AI帮你查数据、看日志、分析代码变更时你会发现开发工作的交互方式正在发生静默但深刻的变革。