10分钟用TypeScript为Cursor构建自定义MCP Server,实现AI编程助手深度集成

📅 2026/8/27 3:44:36
10分钟用TypeScript为Cursor构建自定义MCP Server,实现AI编程助手深度集成
1. 项目概述为什么MCP和Cursor的组合值得你投入10分钟如果你是一个重度使用Cursor的开发者或者对AI编程助手如何更深度地融入你的工作流感到好奇那么“用TypeScript给Cursor写一个自定义MCP Server”这个想法可能就是你一直在寻找的突破口。这不仅仅是一个技术实验它关乎效率的质变。想象一下当你在Cursor里编写一个需要查询数据库的API时不再需要切出IDE去打开数据库客户端或终端而是直接问Cursor“帮我查一下用户表里最近一周的活跃用户”它就能基于实时数据给你返回准确的代码片段甚至分析结果。这个场景的背后核心就是MCPModel Context Protocol协议。MCP可以理解为AI模型比如Cursor背后的Claude与外部工具、数据源之间的一座标准化桥梁。它定义了模型如何“请求”工具执行某个操作如运行SQL查询以及工具如何“响应”返回结果。而Cursor作为一款深度集成AI的IDE已经原生支持了MCP客户端。这意味着只要我们按照MCP的规范实现一个服务端Server就能让Cursor获得访问我们私有数据或服务的能力比如公司内部的数据库、特定的API接口甚至是本地文件系统里的特定文档。为什么选择TypeScript因为它足够流行生态完善特别是对于全栈或Node.js开发者来说几乎没有学习成本。整个构建过程从初始化项目到实现核心协议再到最终集成其核心步骤完全可以压缩在10分钟左右。这10分钟的投入换来的是一个可以无限扩展的、专属于你的AI编程副驾驶。接下来我会带你从零开始拆解每一个环节包括协议核心、项目搭建、关键实现和避坑指南让你不仅能复现更能理解其设计精髓从而定制出更强大的专属工具。2. MCP协议核心思想与项目架构设计在动手写代码之前我们必须先吃透MCP协议的设计哲学。它不是一个复杂的RPC框架其核心目标极其明确为AI模型提供一个安全、可控、标准化的方式来调用外部功能。你可以把它想象成给AI模型定义了一套“技能调用说明书”。2.1 MCP协议的三层核心模型MCP协议主要围绕三个核心概念构建工具Tools、资源Resources和提示词模板Prompts。对于我们的数据库Server场景最相关的是“工具”。工具Tools这是Server向ClientCursor暴露的可调用函数。每个工具都有名称、描述、输入参数模式JSON Schema。当用户在Cursor中提出相关需求时模型会判断是否需要调用某个工具并按照模式传入参数。例如我们可以定义一个名为query_database的工具它接收一个sql字符串参数。模型理解了用户意图后就会构造一个包含有效SQL语句的请求发给我们的Server。资源Resources代表可通过URI引用的只读数据块。例如你可以暴露一个file:///project/schema.sql资源让模型在需要时读取数据库结构文档。这对于提供上下文信息非常有用。提示词模板Prompts预定义的提示词片段模型可以将其组合到对话中。比如一个“优化SQL查询”的模板。对于初期的数据库Server我们聚焦于实现“工具”即可。整个交互流程是CursorMCP Client启动时会连接到我们编写的Server。Server会告知Client“我提供了这些工具比如query_database,list_tables。” 当用户在Cursor聊天框中输入“查询订单表”时Cursor内部的模型会决定调用list_tables工具Server执行后返回表列表。模型可能接着问用户要查哪个表或者直接调用query_database工具并尝试生成一个查询语句。2.2 技术选型与项目骨架我们将使用TypeScript和Node.js环境。关键依赖是官方提供的modelcontextprotocol/sdk包它封装了MCP协议通信的底层细节如SSE传输、请求响应格式让我们可以专注于业务逻辑。首先初始化项目并安装核心依赖mkdir mcp-database-server cd mcp-database-server npm init -y npm install typescript ts-node types/node --save-dev npm install modelcontextprotocol/sdk接着创建基础文件结构mcp-database-server/ ├── src/ │ ├── index.ts # Server入口文件 │ ├── tools/ # 工具实现目录 │ │ └── queryTool.ts │ └── db/ # 数据库连接与操作封装 │ └── client.ts ├── tsconfig.json └── package.json在tsconfig.json中配置基本的编译选项{ compilerOptions: { target: ES2022, module: commonjs, outDir: ./dist, rootDir: ./src, strict: true, esModuleInterop: true, skipLibCheck: true, forceConsistentCasingInFileNames: true }, include: [src/**/*], exclude: [node_modules] }这个结构清晰地将协议处理、工具定义和数据库操作分离。modelcontextprotocol/sdk将负责处理与Cursor之间基于stdin/stdout或SSE的通信我们只需要注册工具并处理工具调用请求。3. 逐步实现构建一个基础的数据库查询Server现在我们开始编写核心代码。整个过程分为建立Server实例、定义并注册工具、实现数据库连接逻辑三个主要步骤。3.1 初始化MCP Server实例在src/index.ts中我们首先导入SDK并创建一个Server实例。Server需要配置一个名称这个名称会在Cursor的连接列表中显示。import { Server } from modelcontextprotocol/sdk/server/index.js; import { StdioServerTransport } from modelcontextprotocol/sdk/server/stdio.js; // 注意SDK可能使用ES模块因此需要添加.js扩展名或在tsconfig中设置moduleResolution: node16 // 创建Server实例 const server new Server( { name: my-database-server, version: 0.1.0, }, { capabilities: { tools: {}, // 声明我们支持提供工具 }, } );这里创建了一个最基本的Server并声明了其具备提供tools的能力。StdioServerTransport是用于标准输入输出的传输层这是Cursor与本地Server通信最常用的方式。3.2 定义并注册第一个查询工具工具是核心。我们定义一个执行原始SQL查询的工具。首先在src/tools/queryTool.ts中定义工具的描述和处理函数。import { Tool } from modelcontextprotocol/sdk/types.js; import { executeQuery } from ../db/client.js; export const queryDatabaseTool: Tool { name: query_database, // 工具的唯一标识符 description: 执行一个只读的SQL查询语句并返回结果。请确保SQL语法正确且仅用于查询。, inputSchema: { type: object, properties: { sql: { type: string, description: 要执行的SELECT查询语句, }, }, required: [sql], additionalProperties: false, }, }; // 工具的处理函数与工具定义分离将在Server中绑定 export async function handleQueryDatabase(args: { sql: string }): Promise{ content: any[] } { const { sql } args; // 简单的安全校验仅允许SELECT查询根据实际情况可调整 if (!sql.trim().toUpperCase().startsWith(SELECT)) { throw new Error(Only SELECT queries are allowed for safety.); } try { const results await executeQuery(sql); return { content: results, // 返回查询结果 }; } catch (error: any) { throw new Error(Database query failed: ${error.message}); } }这里的关键是inputSchema它用JSON Schema严格定义了调用此工具时必须传入的参数格式。这不仅是给Cursor看的“说明书”也是第一道安全防线。然后我们在src/index.ts中注册这个工具import { queryDatabaseTool, handleQueryDatabase } from ./tools/queryTool.js; // 注册工具 server.setRequestHandler(tools/list, async () { return { tools: [queryDatabaseTool], }; }); // 处理工具调用请求 server.setRequestHandler(tools/call, async (request) { if (request.params.name queryDatabaseTool.name) { const result await handleQueryDatabase(request.params.arguments as { sql: string }); return { content: [ { type: text, text: JSON.stringify(result.content, null, 2), // 将结果格式化为美观的JSON字符串 }, ], }; } throw new Error(Unknown tool: ${request.params.name}); });注意在真实场景中直接执行用户或模型生成的原始SQL是极度危险的即使限制了SELECT。这里仅为演示。生产环境必须使用参数化查询、严格的权限控制、SQL解析白名单等机制。一个更安全的做法是暴露具体的工具如get_user_by_id、get_recent_orders而非通用的query_database。3.3 实现安全的数据库连接层为了安全性和可维护性我们将数据库操作封装在独立的模块中。以PostgreSQL为例使用pg库。npm install pg npm install -D types/pg创建src/db/client.tsimport { Pool } from pg; import dotenv from dotenv; dotenv.config(); // 从环境变量读取配置避免硬编码敏感信息 const pool new Pool({ host: process.env.DB_HOST || localhost, port: parseInt(process.env.DB_PORT || 5432), database: process.env.DB_NAME, user: process.env.DB_USER, password: process.env.DB_PASSWORD, max: 5, // 连接池大小 idleTimeoutMillis: 30000, }); // 封装查询函数 export async function executeQuery(sql: string, params: any[] []) { const client await pool.connect(); try { const result await client.query(sql, params); // 使用参数化查询防止SQL注入 return result.rows; } finally { client.release(); // 确保连接释放回连接池 } } // 可选实现一个更安全的“列出所有表”的工具 export async function listTables() { const result await executeQuery( SELECT table_name FROM information_schema.tables WHERE table_schema public ORDER BY table_name; ); return result.map((row) row.table_name); }记得在项目根目录创建.env文件并加入.gitignore来管理数据库凭证DB_HOSTlocalhost DB_PORT5432 DB_NAMEmydb DB_USERmyuser DB_PASSWORDmypassword3.4 启动Server并连接Cursor最后在src/index.ts的末尾启动Server并配置传输层。async function main() { // 创建标准IO传输 const transport new StdioServerTransport(); await server.connect(transport); console.error(MCP Database Server is running on stdio...); } main().catch((error) { console.error(Server fatal error:, error); process.exit(1); });使用ts-node运行开发服务器npx ts-node src/index.ts现在最关键的一步是在Cursor中配置这个Server。Cursor的MCP配置通常位于用户目录下的一个JSON文件中如~/.cursor/mcp.json具体路径请参考Cursor文档。添加如下配置{ mcpServers: { my-database-server: { command: node, args: [ /absolute/path/to/your/mcp-database-server/dist/index.js ], env: { DB_HOST: localhost, DB_USER: myuser, // ... 其他环境变量 } } } }重要提示你需要先用tsc将TypeScript编译成JavaScript到dist目录或者配置ts-node在配置中。更推荐在package.json中配置build脚本并让Cursor指向编译后的产物这样性能更好。配置完成后重启Cursor。理论上Cursor会在启动时运行你的Server命令。你可以在Cursor的聊天框中尝试输入“你能用什么工具”或者“请列出数据库中的所有表”。如果一切正常Cursor会识别并调用你注册的工具。4. 进阶实现提升工具实用性与安全性基础版本虽然能跑通但离“实用”还有距离。我们需要从工具设计、错误处理和性能方面进行强化。4.1 设计更符合AI交互模式的工具直接让AI写SQL不仅危险而且对于复杂查询效果不佳。更好的模式是提供语义化的、受限的工具。例如我们可以将通用的query_database拆解// src/tools/safeTools.ts export const getTableSchemaTool: Tool { name: get_table_schema, description: 获取指定数据表的列名、数据类型和注释。, inputSchema: { type: object, properties: { tableName: { type: string, description: 需要查看结构的表名, }, }, required: [tableName], }, }; export const runParameterizedQueryTool: Tool { name: run_parameterized_query, description: 执行一个预定义的安全查询模板。当前支持get_recent_users(days), get_order_stats(startDate, endDate)。, inputSchema: { type: object, properties: { queryTemplate: { type: string, enum: [get_recent_users, get_order_stats], // 严格限定可选的查询 description: 预定义的查询模板名称, }, parameters: { type: object, description: 传递给查询模板的参数, // 可以为每个模板定义更详细的schema }, }, required: [queryTemplate], }, };对应的处理函数中通过queryTemplate映射到具体的、安全的SQL语句const QUERY_TEMPLATES: Recordstring, string { get_recent_users: SELECT id, username, email, created_at FROM users WHERE created_at NOW() - INTERVAL $1 DAY ORDER BY created_at DESC LIMIT 100, get_order_stats: SELECT status, COUNT(*) as count, SUM(total_amount) as total FROM orders WHERE order_date BETWEEN $1 AND $2 GROUP BY status, }; async function handleParameterizedQuery(args: { queryTemplate: string; parameters?: any }) { const { queryTemplate, parameters } args; const sqlTemplate QUERY_TEMPLATES[queryTemplate]; if (!sqlTemplate) { throw new Error(Unsupported query template: ${queryTemplate}); } // 将参数转换为数组适配pg的参数化查询 const paramsArray parameters ? Object.values(parameters) : []; return await executeQuery(sqlTemplate, paramsArray); }这种方式将控制权牢牢掌握在开发者手中AI只能在你画好的“安全区”内操作实用性反而更高因为它能更可靠地获取结构化数据。4.2 健壮的错误处理与日志记录MCP Server需要非常稳定。我们需要在全局和工具层面做好错误处理。全局未捕获错误处理process.on(uncaughtException, (error) { console.error(Uncaught Exception:, error); // 执行必要的清理如关闭数据库连接池 pool.end().finally(() process.exit(1)); }); process.on(unhandledRejection, (reason, promise) { console.error(Unhandled Rejection at:, promise, reason:, reason); });工具调用层的结构化错误返回MCP协议期望工具调用返回固定的格式。即使在错误情况下我们也应返回一个合法的响应而不是让Server崩溃。server.setRequestHandler(tools/call, async (request) { try { if (request.params.name getTableSchemaTool.name) { // ... 处理逻辑 return { content: [{ type: text, text: JSON.stringify(result) }] }; } // ... 其他工具 throw new Error(Unknown tool: ${request.params.name}); } catch (error: any) { // 返回一个包含错误信息的响应 return { content: [ { type: text, text: Error executing tool ${request.params.name}: ${error.message}, }, ], isError: true, // MCP协议中可能用其他字段表示错误需查阅最新规范 }; } });添加请求日志为了方便调试可以记录每个工具的调用请求和耗时。server.setRequestHandler(tools/call, async (request) { const startTime Date.now(); const toolName request.params.name; console.error([MCP Tool Call] ${toolName} started, request.params.arguments); try { const result await handleToolCall(request); // 你的处理函数 const duration Date.now() - startTime; console.error([MCP Tool Call] ${toolName} succeeded in ${duration}ms); return result; } catch (error) { const duration Date.now() - startTime; console.error([MCP Tool Call] ${toolName} failed in ${duration}ms, error); throw error; // 或返回错误响应 } });4.3 性能优化连接池与请求队列数据库连接是宝贵资源。我们已经在db/client.ts中使用了pg.Pool。此外如果工具可能执行长时间查询需要考虑异步处理和取消机制。虽然MCP协议本身可能对请求超时有约定但Server端也应设置查询超时。import { timeout } from node:timers/promises; async function executeQueryWithTimeout(sql: string, params: any[], timeoutMs 10000) { const client await pool.connect(); try { // 为查询设置超时 const result await Promise.race([ client.query(sql, params), timeout(timeoutMs).then(() { throw new Error(Query timeout); }) ]); return result.rows; } finally { client.release(); } }对于高并发场景虽然本地Cursor调用通常不高可以使用简单的队列来序列化对同一资源的访问避免连接池耗尽或产生竞态条件。5. 调试技巧、常见问题与排查指南即使按照步骤操作第一次集成时也难免遇到问题。以下是几个最常见的坑和解决方法。5.1 Cursor无法识别或连接Server这是最常见的问题。请按以下清单排查检查配置文件路径和格式确保mcp.json文件在Cursor的正确配置目录下并且JSON格式正确没有尾随逗号。可以尝试用JSONLint验证。检查命令路径args中的Node.js路径和你的脚本路径必须是绝对路径。相对路径在Cursor的启动环境中很可能无法解析。使用pwd命令获取你项目dist/index.js的绝对路径。检查环境变量在mcp.json的env字段中传递数据库密码等敏感信息或者确保Server进程能从其运行环境如系统环境变量、.env文件中读取到。可以在Server启动的第一行打印process.env.DB_HOST来验证。查看Cursor日志Cursor通常会有输出日志的地方如开发者控制台或特定的日志文件。查看是否有关于MCP Server启动失败的错误信息。启动Cursor时从终端启动有时也能看到相关输出。手动测试Server首先确保你的Server能独立运行。在项目目录下运行node dist/index.js。它应该启动并等待在标准输入上而不是立即退出。如果立即退出说明代码有未捕获的异常检查上面的错误处理。验证通信协议MCP协议基于JSON-RPC over SSE或stdio。一个简单的测试是在Server代码中监听server.on(‘request’, ...)事件打印收到的所有请求看Cursor启动时是否发来了initialize和tools/list请求。5.2 工具被列出但调用失败参数格式不匹配这是最可能的原因。仔细检查工具inputSchema的定义和Cursor实际发送的arguments是否完全匹配。特别是required字段和additionalProperties: false的设置。可以在工具调用处理函数中打印request.params.arguments来确认。异步处理未返回确保你的工具处理函数是async的并且正确使用了await。如果函数内部有未处理的Promise拒绝可能导致请求无响应。错误未妥善处理参考4.2节确保工具函数内部的错误被捕获并转化为MCP协议认可的响应格式而不是抛出到SDK外层导致连接中断。权限问题你的数据库用户是否有权执行你发送的查询在Server日志中检查数据库返回的具体错误。5.3 性能问题或Cursor卡顿查询太慢如果查询需要数秒Cursor的AI响应也会被阻塞。务必为查询添加合理的索引并在工具描述中提醒AI“此操作可能较慢”。考虑实现查询超时见4.3节。连接泄漏确保每次pool.connect()后在finally块中或使用try...catch...finally结构都执行了client.release()。未释放的连接会迅速占满连接池导致后续请求挂起。Server资源占用使用console.time和console.timeEnd对关键函数进行简单的性能分析找出瓶颈。5.4 安全加固清单在将任何自定义MCP Server用于生产环境或处理敏感数据前请务必检查[ ]禁用通用SQL工具使用具体的、参数化的工具模板替代query_database。[ ]最小权限原则数据库连接用户只拥有只读权限且仅限于必要的表和视图。[ ]输入验证与净化即使使用参数化查询也应对传入的参数进行类型和范围检查例如days参数不能为负数。[ ]访问控制考虑在Server端添加简单的API密钥验证如果MCP传输层是本地stdio则非必须如果是网络SSE则必须。[ ]日志脱敏确保日志中不会记录完整的SQL语句可能包含数据或敏感的查询参数。[ ]依赖安全定期更新pg、modelcontextprotocol/sdk等依赖避免已知漏洞。6. 扩展思路从数据库Server到个人知识库助手一旦掌握了构建基础MCP Server的方法你就可以打开思路将其扩展到更多场景让Cursor真正成为你的全能助手。场景一内部API集成。为公司内部的管理系统、CRM、项目管理系统构建MCP Server。暴露如get_jira_issue、create_github_pr、fetch_sales_data等工具。这样在写代码涉及业务逻辑时可以直接让Cursor查询实时业务数据。场景二本地文档知识库。利用resources特性将你本地的项目文档、API手册、个人笔记以资源的形式暴露给Cursor。模型可以在需要时引用这些文档的具体内容提供更精准的代码建议或解答。场景三复杂工作流自动化。将一个需要多步骤的操作如“部署到测试环境”封装成一个工具。Server后端可以执行一系列脚本拉取代码、运行测试、构建镜像、触发部署。你只需要在Cursor里说一句“请将当前分支部署到 staging 环境。”实现这些扩展技术栈是相通的。核心在于定义清晰的工具边界和输入模式让AI能准确理解何时以及如何调用。构建安全可靠的后端执行层处理认证、错误和异步任务。提供丰富且结构化的上下文通过工具描述和资源内容帮助AI做出最佳决策。构建自定义MCP Server的过程本质上是在为你和AI的协作方式设计接口。从接入数据库这个最实用的点切入花上10分钟跑通流程你获得的不仅是一个工具更是一种将任何能力赋予AI助手的范式。剩下的就是发挥你的想象力去打造那个独一无二的、超级高效的开发工作流了。