10分钟构建自定义MCP Server:让AI助手连接你的数据库

📅 2026/8/9 13:02:27
10分钟构建自定义MCP Server:让AI助手连接你的数据库
1. 项目概述为什么我们需要自定义MCP Server如果你和我一样日常开发重度依赖Cursor这类AI编程助手那你肯定遇到过这样的场景想让它帮你写个SQL查询或者分析一下数据库表结构但它总是因为“不了解你的数据库”而给出一些似是而非、甚至完全错误的建议。这感觉就像请了个顶尖厨师来家里做饭结果你告诉他“调料在厨房你自己找”他只能对着空荡荡的灶台干瞪眼。这就是MCPModel Context Protocol要解决的核心问题。简单来说MCP是一个标准协议它允许你将任何外部工具、数据源或服务安全、结构化地“喂”给像Cursor、Claude Desktop这样的AI助手。你可以把它想象成AI的“USB接口”或“插件系统”。通过这个接口AI就能读取你的数据库、调用你的API、操作你的文件系统从而获得真正与你工作环境相关的上下文给出精准得多的答案。而今天我们要做的就是亲手打造一个这样的“接口”——一个自定义的MCP Server。具体来说我们将用TypeScript编写一个Server让它能够连接到你本地的数据库比如SQLite、PostgreSQL并将数据库的结构表、字段甚至查询能力暴露给Cursor。整个过程从零开始目标是在10分钟左右跑通一个可工作的原型。这不仅仅是接个数据库那么简单更是打开了一扇门让你能按需定制AI的“知识边界”。下面我就带你一步步拆解实现。2. MCP协议核心概念与工作原理解析在动手写代码之前我们得先搞清楚MCP这套“交通规则”是怎么运行的。否则很容易写出一堆能跑但不知道为啥能跑的“黑盒”代码。2.1 MCP的三层架构Server、Transport与ClientMCP的交互模型非常清晰主要涉及三个角色MCP Server我们即将构建的这是信息的提供方。它封装了对特定资源如数据库、文件系统、API的访问逻辑并按照MCP协议定义的方式将这些资源“工具化”和“资源化”。Transport传输层这是Server和Client之间的通信桥梁。MCP支持两种主要方式stdio标准输入输出和SSEServer-Sent Events。对于我们这种本地化、轻量级的集成stdio是最简单直接的选择。我们的Server进程启动后通过stdin接收JSON-RPC请求通过stdout输出JSON-RPC响应。MCP Client如Cursor、Claude Desktop这是信息的消费方。它启动Server进程并通过Transport与之通信调用Server提供的工具或读取Server暴露的资源。整个流程可以类比为点餐你Client在餐厅你的电脑里向服务员Transport下单。服务员把订单JSON-RPC请求送到后厨Server。后厨根据订单调用对应工具做好菜再通过服务员把菜JSON-RPC响应端给你。2.2 协议核心JSON-RPC与资源/工具模型MCP基于JSON-RPC 2.0规范这是一种轻量级的远程过程调用协议。所有的请求和响应都是结构化的JSON对象。这保证了通信的标准化和跨语言兼容性。MCP Server主要向Client暴露两种东西资源Resources可以理解为“只读的数据”。比如一个数据库表的模式Schema定义。Client可以“读取”它。资源通过一个唯一的URI来标识例如db://schema/users。工具Tools可以理解为“可执行的操作”。比如“执行一条SQL查询”。Client可以“调用”它并传入参数如SQL语句。工具执行后可能会返回结果也可能会产生副作用如修改数据。我们的数据库MCP Server核心就是要暴露一个资源db://schema用于列出所有表的结构。一个工具execute_query用于执行用户输入的SQL语句。2.3 为什么选择TypeScript你可能会问用Python、Go不行吗当然可以MCP协议是语言无关的。但我选择TypeScript基于以下几点考虑生态成熟Node.js环境部署极其简单npm包管理器海量的库能让我们快速连接各种数据库sqlite3,pg,mysql2。类型安全TypeScript的静态类型检查对于构建需要严格遵循协议格式JSON Schema的Server来说是巨大的优势。它能极大减少因字段名拼写错误、类型不匹配导致的运行时bug。开发体验有官方维护的modelcontextprotocol/sdk包它封装了底层的JSON-RPC通信、资源与工具的定义等繁琐细节让我们能专注于业务逻辑。与前端生态契合很多使用Cursor的开发者本身就在用Node.js/TS技术栈环境统一学习成本低。理解了这些我们就知道代码该往哪个方向写了。接下来我们进入实战环节。3. 10分钟快速实战构建基础数据库MCP Server我们来设定一个最小可行目标构建一个能连接SQLite数据库并能响应“列出表结构”和“执行查询”两个基本功能的Server。SQLite无需安装服务器一个文件就是一个数据库最适合快速演示。3.1 环境准备与项目初始化首先确保你的机器上安装了Node.js版本18或以上和npm。然后打开终端开始我们的“十分钟倒计时”。# 1. 创建项目目录并进入 mkdir mcp-database-server cd mcp-database-server # 2. 初始化npm项目一路回车用默认值即可 npm init -y # 3. 安装核心依赖 npm install modelcontextprotocol/sdk sqlite3 # 4. 安装TypeScript及相关类型定义开发依赖 npm install -D typescript types/node tsx # 5. 初始化TypeScript配置 npx tsc --init安装的包作用如下modelcontextprotocol/sdk MCP官方SDK核心。sqlite3 用于操作SQLite数据库。typescript,types/node TypeScript编译器和Node.js类型定义。tsx 一个TypeScript运行时可以直接运行.ts文件省去编译步骤适合开发。接下来修改package.json添加一个启动脚本方便我们后续运行{ name: mcp-database-server, version: 1.0.0, scripts: { start: tsx server.ts }, dependencies: { modelcontextprotocol/sdk: ^1.0.0, sqlite3: ^5.1.6 }, devDependencies: { types/node: ^20.0.0, tsx: ^4.7.0, typescript: ^5.0.0 } }3.2 编写核心Server代码在项目根目录创建server.ts文件。我们将分步填充代码。第一步导入依赖并创建Server实例import { Server } from modelcontextprotocol/sdk/server/index.js; import { StdioServerTransport } from modelcontextprotocol/sdk/server/stdio.js; import Database from sqlite3; // 1. 创建Server实例并声明其能力提供哪些资源、工具 const server new Server( { name: mcp-database-server, version: 1.0.0, }, { capabilities: { resources: {}, // 我们先留空后面会添加 tools: {}, // 我们先留空后面会添加 }, } );这里我们创建了Server对象并给它起了个名字和版本。capabilities字段非常重要它像一份“菜单”告诉ClientCursor我们这个Server能提供什么。目前菜单是空的。第二步连接数据库并定义工具我们假设数据库文件在当前目录名为example.db。在实际项目中这个路径可以通过环境变量或配置文件传入。// 2. 连接SQLite数据库以只读模式打开安全起见 const db new Database.Database(./example.db, Database.OPEN_READONLY, (err) { if (err) { console.error(Failed to connect to database:, err.message); process.exit(1); } console.error(MCP Server: Connected to SQLite database.); // 使用stderr输出日志避免污染stdout协议流 }); // 3. 定义第一个工具执行SQL查询 server.setRequestHandler(tools/call, async (request) { // 检查Client调用的工具名是否是我们定义的 if (request.params.name execute_query) { const sql request.params.arguments?.sql as string; // 从参数中获取SQL语句 if (!sql) { throw new Error(SQL query is required.); } // 为了安全这里可以添加简单的SQL验证例如禁止DROP, DELETE等操作。 // 这是一个简易的演示生产环境需要更严格的权限控制。 const upperSql sql.toUpperCase(); if (upperSql.includes(DROP ) || upperSql.includes(DELETE ) || upperSql.includes(INSERT ) || upperSql.includes(UPDATE )) { return { content: [ { type: text, text: For safety, the execution of SQL containing DROP, DELETE, INSERT, or UPDATE is blocked in this demo server., }, ], }; } // 执行查询 return new Promise((resolve, reject) { db.all(sql, [], (err, rows) { if (err) { reject(new Error(Query failed: ${err.message})); } else { // 将查询结果格式化为MCP要求的响应格式 const resultText rows.length 0 ? Query executed successfully. No rows returned. : JSON.stringify(rows, null, 2); // 美化输出JSON resolve({ content: [ { type: text, text: resultText, }, ], }); } }); }); } // 如果调用的是其他未定义的工具抛出错误 throw new Error(Unknown tool: ${request.params.name}); });这段代码做了几件事连接到一个本地的SQLite文件。注册了一个对tools/call请求的处理器。当Cursor调用工具时就会触发这个函数。在处理器内部我们检查工具名是否为execute_query然后从参数中提取SQL语句。添加了一个极其基础的安全检查阻止一些可能危险的写操作。请注意这只是一个演示绝对不足以用于生产环境生产环境需要基于角色、查询白名单等更复杂的机制。使用db.all执行查询并将结果包装成MCP规定的格式返回。content是一个数组其中可以包含文本(text)、图片(image)等类型我们这里只返回文本。第三步定义资源列出数据库表除了工具我们还可以暴露资源。让我们添加一个资源让Cursor能“看到”数据库里有哪些表。// 4. 定义资源获取数据库模式表列表 server.setRequestHandler(resources/list, async () { // 查询SQLite的元数据表 sqlite_master 来获取所有表名 return new Promise((resolve, reject) { db.all(SELECT name, type FROM sqlite_master WHERE typetable AND name NOT LIKE sqlite_%, [], (err, tables) { if (err) { reject(new Error(Failed to list tables: ${err.message})); } else { // 将每个表定义为一个资源 const resources tables.map((table: any) ({ uri: db://schema/${table.name}, mimeType: application/json, name: Schema of table: ${table.name}, description: The structure of the database table ${table.name}, })); resolve({ resources, }); } }); }); }); // 5. 定义资源内容处理器当Client请求某个具体表的资源时返回其结构 server.setRequestHandler(resources/read, async (request) { const uri request.params.uri as string; // 检查请求的URI是否符合我们的模式 db://schema/{tableName} if (uri.startsWith(db://schema/)) { const tableName uri.substring(db://schema/.length); // 查询表的创建语句其中包含了列定义 return new Promise((resolve, reject) { db.get(SELECT sql FROM sqlite_master WHERE typetable AND name ?, [tableName], (err, row: any) { if (err) { reject(new Error(Failed to read table schema: ${err.message})); } else if (!row) { reject(new Error(Table ${tableName} not found.)); } else { resolve({ contents: [ { uri, mimeType: application/json, text: JSON.stringify({ sql: row.sql }, null, 2), }, ], }); } }); }); } throw new Error(Resource not found: ${uri}); });这里我们注册了两个资源相关的处理器resources/list: 当Client请求列出所有资源时我们查询数据库将每个表作为一个资源返回并赋予它一个唯一的URI如db://schema/users。resources/read: 当Client根据URI请求某个特定资源如db://schema/users的内容时我们查询该表的创建SQL语句并返回这清晰地展示了表的结构。第四步启动Server并连接传输层最后我们需要启动Server并告诉它使用stdio标准输入输出作为传输方式。这是MCP Client如Cursor期望的通信方式。// 6. 启动Server使用stdio传输 async function runServer() { const transport new StdioServerTransport(); await server.connect(transport); console.error(MCP Database Server is now running and listening via stdio...); } runServer().catch((error) { console.error(Server fatal error:, error); process.exit(1); });至此一个最基础的、功能完整的MCP Database Server就写好了。完整的server.ts文件大约在100行左右。3.3 测试与运行在运行之前我们需要一个测试用的数据库。在项目根目录创建一个example.db文件并用任何SQLite工具如sqlite3命令行或VS Code的SQLite插件执行以下SQL来创建示例数据-- 创建示例表 CREATE TABLE users ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT NOT NULL UNIQUE, email TEXT NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE orders ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, product_name TEXT NOT NULL, amount REAL NOT NULL, FOREIGN KEY (user_id) REFERENCES users(id) ); -- 插入一些示例数据 INSERT INTO users (username, email) VALUES (alice, aliceexample.com); INSERT INTO users (username, email) VALUES (bob, bobexample.com); INSERT INTO orders (user_id, product_name, amount) VALUES (1, Laptop, 1299.99); INSERT INTO orders (user_id, product_name, amount) VALUES (2, Mouse, 29.99);现在你可以直接运行我们的Server来测试它是否正常工作npm start如果看到MCP Database Server is now running and listening via stdio...的输出说明Server已经启动并在等待连接。目前它是在前台运行等待通过stdin接收JSON-RPC命令。要手动测试我们可以写一个简单的测试脚本但更直接的方式是配置Cursor来使用它。4. 配置Cursor接入你的MCP ServerServer写好了怎么让Cursor知道并使用它呢这需要通过Cursor的配置文件来完成。第一步找到Cursor的MCP配置文件Cursor的配置通常位于以下位置macOS/Linux:~/.cursor/mcp.jsonWindows:%USERPROFILE%\.cursor\mcp.json如果文件或目录不存在手动创建即可。第二步编辑mcp.json配置文件在这个JSON文件中你可以配置多个MCP Server。我们添加刚刚创建的数据库Server。{ mcpServers: { my-database-server: { command: node, args: [ /ABSOLUTE/PATH/TO/YOUR/PROJECT/mcp-database-server/server.ts ], env: { NODE_PATH: /ABSOLUTE/PATH/TO/YOUR/PROJECT/node_modules } } } }关键配置项解释my-database-server: 这是你给这个Server起的任意名字。command: 启动Server的命令。因为我们用tsx来直接运行TypeScript所以这里也可以是npx但更推荐使用绝对路径的node配合编译后的JS文件或者直接使用tsx。为了简单我们可以先用node运行编译后的JS。我们先按上述配置后面会讲优化。args: 传递给命令的参数。这里指向我们server.ts文件的绝对路径。注意必须使用绝对路径env: 可选设置环境变量。有时需要指定NODE_PATH来让Server找到项目本地的node_modules。更优的配置方式使用npm script和相对路径首先在package.json中确保有start脚本我们之前已经加了。然后在mcp.json中这样配置{ mcpServers: { my-database-server: { command: npm, args: [run, start], cwd: /ABSOLUTE/PATH/TO/YOUR/PROJECT/mcp-database-server } } }这种方式更清晰因为它利用了项目自身的package.json配置。cwd字段指定了命令在哪个目录下执行。第三步重启Cursor并验证保存mcp.json文件后完全关闭并重新启动Cursor。这是关键的一步因为Cursor通常在启动时读取配置。重启后打开Cursor。当你新建一个对话时你应该能在输入框附近或设置里看到相关的MCP工具被加载的提示。更直接的测试方法是在聊天框中输入一些自然语言比如“帮我查看一下数据库里有哪些表” “执行一个查询SELECT * FROM users;”如果配置成功Cursor会识别到你配置的MCP工具并能够调用execute_query工具来执行SQL或者读取db://schema资源来获取表结构。它可能会在回复中说明“正在使用数据库工具”并展示查询结果。5. 进阶优化与生产环境考量十分钟的原型跑通了但要把这个东西真正用在日常工作中我们还需要考虑更多。下面是一些关键的优化方向和避坑指南。5.1 安全性加固第一道防线绝不能破我们演示中的SQL拦截非常简陋绝对不可用于任何含有真实数据的场景。以下是一些必须考虑的安全措施严格的权限分离Server连接数据库时必须使用只读权限最低的数据库用户。对于SQLite使用OPEN_READONLY标志。对于PostgreSQL/MySQL创建仅具SELECT权限的专用用户。查询白名单/安全解析对于简单场景可以维护一个允许执行的SQL语句白名单例如只允许SELECT开头的语句且不包含子查询等复杂结构。更安全的方式是使用SQL解析器如sql-parser库对SQL进行语法分析确保其结构合法且仅包含允许的操作。参数化查询与输入净化我们的示例直接拼接了用户输入的SQL。虽然MCP ClientCursor传来的输入相对可信但仍应防范注入。如果工具设计是执行带参数的查询务必使用数据库驱动提供的参数化查询接口如db.all(“SELECT * FROM users WHERE id ?”, [userId])而不是字符串拼接。资源访问控制不是所有表都应该暴露。在resources/list处理器中应根据配置或规则过滤掉系统表、敏感数据表。环境变量管理数据库连接字符串、密码等敏感信息绝不能硬编码在代码中。必须通过环境变量如DATABASE_URL传入并在生产环境中使用密钥管理服务。5.2 功能增强从查询到智能助理基础查询只是开始我们可以让这个Server变得更“聪明”暴露更多工具describe_table table_name: 更友好地描述表结构、字段类型、主键、外键而不仅仅是原始的CREATE SQL。suggest_query natural_language: 接收自然语言描述如“找出消费最高的用户”在Server内部尝试将其转换为SQL并执行。这可以集成一个轻量级的本地LLM或规则引擎。get_table_stats: 返回表的行数、大小等统计信息帮助AI了解数据规模。提供更丰富的资源除了表结构还可以暴露数据字典、ER图以Mermaid格式的text资源提供、常用的查询模板等。支持多种数据库通过配置文件或工具参数让Server能够动态连接MySQL、PostgreSQL等。可以使用像knex或prisma这样的ORM/查询构建器来抽象底层数据库差异。5.3 工程化与可维护性配置化将数据库连接信息、允许的操作、表黑名单等抽取到独立的配置文件如config.yaml或config.json中。日志与监控添加详细的日志记录使用winston或pino记录工具调用、查询执行情况、错误信息等便于调试和审计。可以将日志输出到文件避免污染stdio流。错误处理实现更优雅的错误处理中间件给Client返回结构化的错误信息而不是让进程崩溃。健康检查实现一个简单的健康检查端点或机制方便运维。编译与打包开发时用tsx没问题生产部署建议将TypeScript编译成单一的JavaScript文件可以使用tsup或esbuild进行打包提升启动速度。5.4 连接其他数据源与服务的思路MCP的威力在于其通用性。掌握了数据库Server的构建方法你可以举一反三连接任何东西内部API Server写一个MCP Server封装对公司内部GitLab、Jira、CRM等系统的API调用。让AI能帮你查任务单、创建合并请求。文件系统Server在安全沙盒内让AI能读取特定项目目录下的代码文件结构、配置文件实现更精准的代码理解和生成。云服务Server连接AWS S3、CloudWatch等让AI帮你查询日志、管理资源。组合多个Server你可以在Cursor中配置多个MCP Server。一个管数据库一个管Git一个管部署。AI可以在一次对话中综合运用所有这些工具成为你的全能副驾驶。6. 常见问题与排查技巧实录在实际搭建和配置过程中你几乎一定会遇到下面这些问题。这里是我踩过坑后总结的排查清单。6.1 Server启动失败或Cursor无法连接症状Cursor启动无提示或提示MCP Server错误。排查步骤检查命令路径mcp.json中的command和args或cwd必须是绝对路径。这是最常见的问题。使用pwdLinux/macOS或cdWindows命令获取项目的绝对路径。手动测试Server在终端中切换到项目目录直接运行你配置的命令如npm run start。观察Server是否能正常启动并打印等待日志。如果报错通常是依赖未安装运行npm install或代码语法错误。检查Node版本确保你的Node版本符合要求18。在mcp.json中可以尝试使用node命令的绝对路径。查看Cursor日志Cursor通常会有隐藏的日志文件。在macOS上可以尝试在终端运行/Applications/Cursor.app/Contents/MacOS/Cursor --enable-logging来启动并查看控制台输出。日志中可能会有加载MCP配置失败的具体原因。简化配置初次尝试时mcp.json尽量简单。可以先只配置一个Server确保command是系统中全局可用的如node,python3。6.2 工具调用无响应或报错症状在Cursor里提到了数据库或相关关键词但AI没有调用工具或者调用后返回错误。排查步骤确认工具已注册在你的Server代码中检查server.setRequestHandler(‘tools/call’, …)是否正确处理了工具名。工具名如execute_query必须完全匹配。检查参数格式MCP工具调用的参数是一个对象。确保你在代码中获取参数的路径正确request.params.arguments?.sql。可以在Server代码中添加console.error(JSON.stringify(request))来打印收到的原始请求看看Cursor到底发送了什么。Server日志确保你的Server将调试信息输出到stderr使用console.error而不是stdout。因为stdout是留给JSON-RPC协议通信的如果混入普通日志会导致协议解析失败。在运行Server的终端里观察是否有错误输出。Cursor的“意识”有时你需要更明确地指示Cursor使用工具。尝试在提问时说“请使用数据库工具列出所有表。”或者“调用execute_query工具执行以下SQL...”。AI需要一定的上下文来知道该用什么工具。6.3 性能与稳定性问题症状查询响应慢或Server偶尔崩溃。解决思路数据库连接池对于MySQL/PostgreSQL不要为每个请求创建新连接。使用连接池如mysql2/promise的createPool。查询超时在数据库查询操作上设置超时限制避免一个慢查询拖死整个Server。错误边界用try…catch包裹所有异步操作确保单个请求失败不会导致Server进程退出。在tools/call和resources/read的处理器中做好错误捕获并返回格式化的错误信息给Client。资源限制对于可能返回大量数据的查询实现分页或行数限制。可以在工具参数中增加limit选项。6.4 权限与网络问题连接远程数据库时症状Server能启动但无法连接远程数据库。排查步骤网络连通性在Server运行的机器上用telnet或nc命令测试是否能连接到数据库的主机和端口。防火墙与安全组检查数据库服务器的防火墙规则和安全组如果是在云上确保允许来自MCP Server所在机器的入站连接。SSL连接如果数据库要求SSL连接需要在连接配置中启用。例如在PostgreSQL的pg库中需要设置ssl: { rejectUnauthorized: false }生产环境应使用有效证书。环境变量再次确认连接字符串、用户名、密码是通过环境变量正确传递的并且没有在代码中硬编码或误提交到版本库。构建自定义MCP Server的过程本质上是在为AI扩展感知和操作边界。从简单的数据库查询开始你已经掌握了这套协议的核心玩法。剩下的就是发挥你的想象力将你工作流中那些重复、繁琐、需要上下文信息的环节都封装成一个个MCP工具。当你的Cursor能直接查看你的数据、操作你的系统、调用你的API时那种“人机合一”的流畅感才是智能编程助手带来的真正生产力革命。