MCP Server进阶实践:错误处理、流式输出与远程部署全指南 MCP Server 写起来容易做好却很难我接触 MCPModel Context Protocol已经有段时间了。一开始我也以为MCP Server 就是一个把工具包上一层协议壳的东西写几个 function 就行。但真正动手把服务做到能上线、能扛住真实调用、能被人稳定使用时才发现里面的坑一个接一个错误处理没有统一规范客户端那边拿到的报错信息完全是“天书”流式输出没设计好AI 调用一个耗时工具时体验极其糟糕TypeScript 工程配置不当构建出来的产物在客户端侧根本无法加载部署到远程之后stdio 方式失效HTTP 传输又有一堆协议细节要处理。这篇文章就把我踩过的坑、试过的方案、最终沉淀下来的做法整理成一份完整笔记覆盖错误处理、流式输出、TypeScript 工程化、远程部署这四个进阶方向。不管你是刚入门 MCP 开发还是已经写了几个工具但总觉得代码不够稳这篇文章都值得你花十分钟认真读一遍。1. 开发前必须建立的两个认知框架1.1 先想清楚MCP Server 到底在解决什么问题MCP 本质上是一套“AI 应用与外部工具/数据源之间的标准化通信协议”。它的价值可以类比成 USB-C 接口过去每个 AI 应用要接入不同的工具都得单独写适配代码现在有了统一标准任何支持 MCP 的客户端Claude Desktop、各类 Agent 框架、自研应用都可以用同一套方式去发现和调用你提供的能力。Server 对外暴露的无非是三类能力Tools工具可被模型调用的函数式能力比如查数据库、调第三方 API、执行计算。Resources资源可被读取的数据内容比如文件、文档片段、配置信息。Prompts提示词模板预定义好的交互模板帮助模型以正确姿势处理特定场景。很多人容易把 Agent Skill 和 MCP 混为一谈。我的理解是Agent Skill 更偏向“给 Agent 封装一项技能或工作流程”它关注的是“怎么做这件事”MCP 则是“把现有工具和数据以标准协议暴露出来”它关注的是“如何被标准化调用”。两者不是替代关系实际项目中经常配合使用——MCP 负责打通能力Skill 负责编排用法。1.2 清醒一点这几个难点不是“附加题”是基本功很多人写第一个 MCP Server 时只关注“怎么把函数暴露出去”等真正面对真实用户或真实的大模型调用方时才会意识到错误处理不规范大模型拿到的错误信息无法理解无法自主修正直接导致调用失败。流式输出缺失面对耗时任务客户端一直等不到任何反馈交互体验极差。TypeScript 工程化薄弱类型混乱导致维护困难构建产物出现各种诡异问题。部署方式选错本地好好的服务部署到远程就失效因为 stdio 方式本身就是为“本地进程”设计的。这四项不是进阶之后才考虑的事情。从第一天写 Server 开始就应该用生产级的标准要求自己。下面我会把这四项逐一拆开讲清楚背后的原理和具体的实施方案。1.3 环境准备TypeScript 工程的正确初始化姿势在动手写业务逻辑之前先把工程基础打牢。我推荐用 pnpm TypeScript Node.js 18 的组合SDK 选择官方维护的modelcontextprotocol/sdk。初始化步骤很简单mkdir my-mcp-server cd my-mcp-server pnpm init pnpm add modelcontextprotocol/sdk zod pnpm add -D typescript types/node tsx这里我把zod也加进来了后面讲参数校验时会用到它和 MCP SDK 的配合非常顺畅。tsx用来在开发阶段直接运行 TypeScript 代码省去每次改动都要编译的烦恼最终发布前再用tsc产出干净的编译结果。目录结构建议按“入口 工具注册 工具实现 公共模块”拆分避免把一堆工具逻辑堆在index.ts里。基础结构可以这样src/ index.ts // 服务入口负责创建 Server、注册能力 tools/ // 每个工具一个文件导出工具定义和实现 resources/ // 资源定义与读取逻辑 lib/ // 公共模块错误码、日志、配置等2. 错误处理的正确姿势不要让调用方猜谜2.1 底层逻辑MCP 走的是 JSON-RPC 2.0错误码不是随便定义的MCP 的通信协议建立在 JSON-RPC 2.0 之上这意味着服务端返回的错误必须符合 JSON-RPC 的规范。JSON-RPC 标准定义了几个核心错误码错误码含义使用场景-32700解析错误请求不是合法的 JSON-32600无效请求请求内容不符合 JSON-RPC 规范-32601方法不存在调用的工具/方法未注册-32602无效参数入参校验不通过-32603内部错误服务端执行过程中发生了未预期的异常-32000 及以上服务端自定义错误业务层面的特定错误场景我见过不少 MCP Server 的错误处理就是“一锤子买卖”所有错误都返回internal error或者干脆把底层异常堆栈直接抛出去。前者让调用方完全无法定位问题后者则泄露了服务端内部实现细节既不安全也不专业。2.2 推荐的错误处理实践统一封装分类透传在服务端代码里我建议把所有工具的执行逻辑都包在一个统一错误处理层里对错误做分级处理。核心原则是业务可预期错误明确返回未知异常统一兜底并记录日志。先定义一个自定义错误类export class McpToolError extends Error { constructor( message: string, public readonly code: number -32000, public readonly details?: unknown ) { super(message); this.name McpToolError; } }然后写一个统一的执行包装函数export function withErrorHandling(handler: (args: unknown) unknown) { return async (args: unknown) { try { const result await handler(args); return { content: [ { type: text, text: JSON.stringify(result) }, ], }; } catch (error) { if (error instanceof McpToolError) { // 业务预期错误将错误码和信息透出 return { isError: true, content: [ { type: text, text: ${error.message} }, ], }; } // 未知错误记录日志并返回通用错误信息 console.error([tool_error], error); return { isError: true, content: [ { type: text, text: 服务内部错误请稍后重试 }, ], }; } }; }这里有两个值得展开的设计细节isError: true要显式返回。很多初写 MCP Server 的人不知道这个字段一旦工具内部抛错客户端拿到的不是结构化的错误响应而是传输层面的异常模型完全无法根据错误信息自我修正。业务错误和系统错误严格区分。参数不合法、资源不存在这类错误属于“业务可预期错误”要给出清晰、具体的信息数据库连接失败、第三方 API 超时这类错误属于“系统未知错误”不要把底层细节暴露给调用方而是通过日志记录下来由开发者去排查。2.3 避坑经验我犯过的三个错误处理失误失误一把底层异常直接抛出一开始我写工具时直接让数据库访问的异常向上抛结果客户端拿到的报错像这样“SQLite3Error: no such table: users”。这个信息对调用方没有任何帮助而且暴露了底层存储结构。正确的做法是捕获后转换为“数据访问失败请检查数据源是否存在”这类业务化信息。失误二忽略参数校验MCP 的inputSchema定义了工具入参的标准但很多人只定义了类型不写严格校验。结果模型传进来的参数千奇百怪服务端执行时才发现缺字段、类型错误。我的实践是用 zod 定义 schema在服务端执行业务逻辑之前先做一轮校验不通过的参数直接返回-32602。失误三所有错误都返回同样的信息如果你把所有失败场景都包装成同一句话大模型在调用时就没有依据去调整参数或改变策略。比如一个天气查询工具至少要区分“城市不存在”“API 密钥无效”“上游服务超时”这几种情况模型才能根据提示做出正确的下一步选择。3. 流式输出设计让耗时任务不再“干等”3.1 流式输出到底指什么这里要澄清一个关键概念MCP 中的流式输出和 OpenAI 那种 token 级流式输出不是一回事。OpenAI 的流式是“文本生成过程中逐字吐出结果”MCP 的流式输出更接近“长连接上持续推送任务状态和阶段性结果”。MCP 的 SDK 在设计上允许 Server 通过多次返回“内容块content block”的方式实现渐进式响应。也就是说工具调用不一定要一次性返回最终结果可以分多次返回进度信息、中间状态最后再返回完整结果。3.2 设计原则短任务直接返回长任务边跑边报我的经验法则是如果一个任务能在几百毫秒内完成直接返回最终结果就好不要画蛇添足做流式如果一个任务耗时可能是几秒甚至几十秒比如查一堆数据库、调多个第三方 API、跑一次 RAG 检索就必须做渐进式反馈。为什么因为当 AI 应用调用 MCP Server 时用户往往盯着界面等待。如果几秒内没有反馈用户的第一反应是“卡死了”。而在 Agent 自动化调用的场景下长时间无响应可能导致客户端超时甚至判定任务失败。3.3 实操方案从普通工具到流式工具在 MCP SDK 中工具返回的结构是content数组。普通工具一次返回最终结果流式工具则分多次推送。const searchTool { name: deep_search, description: 执行多数据源深度搜索适合耗时较长的查询任务, inputSchema: { type: object, properties: { query: { type: string }, }, required: [query], }, async execute(args: { query: string }, emit: (content: unknown) void) { emit({ type: text, text: 已收到搜索请求开始连接数据源... }); await searchSourceA(args.query); emit({ type: text, text: 数据源A检索完成命中 3 条结果继续检索数据源B... }); await searchSourceB(args.query); emit({ type: text, text: 数据源B检索完成正在聚合去重... }); const finalResults mergeAndRank(args.query); emit({ type: text, text: 搜索完成共 ${finalResults.length} 条结果。, }); return { content: [ { type: text, text: JSON.stringify(finalResults) }, ], }; }, };这里要特别说明emit的调用并不意味着立刻返回给客户端它更像是在任务进行中持续推送“进度事件”最终的return才是真正的工具调用结果。实际实现时不同 SDK 版本可能细节略有差异请以官方文档为准。3.4 流式设计中的几个关键细节进度信息要有“阶段性”。不要只报“正在处理”要明确告诉调用方“正在做什么、已经完成了什么”。这不仅是用户体验问题也是 Agent 调用的上下文质量问题——模型可以从中间信息中判断是否需要继续等待。中间消息和最终结果要结构清晰。中间进度消息是给人或模型看的叙述性文本最终结果是结构化数据。两者要区分开不能混在一起否则模型拿到结果时无法区分“过程”和“结论”。长任务要有超时和取消机制。流式输出不是“无限等待”的借口。如果某个任务超过设定阈值比如 60 秒应该主动结束并返回部分结果或超时信息。另外客户端如果已经放弃等待Server 端要能感知到并终止任务执行避免资源泄漏。我在实际项目里踩过一个坑没有实现取消机制结果某个耗时任务因为上游 API 无响应导致 Server 的进程一直挂着最后内存被打满。后来在实现层加了AbortController把每个任务和取消信号绑定才彻底解决这个问题。4. TypeScript 工程化从“能跑”到“好维护”4.1 严格模式不是可选配置是底线MCP Server 的代码往往涉及各种外部数据源的类型定义如果 TypeScript 的严格模式没开类型检查形同虚设编译期间逃过的问题都会在运行时爆发。我建议在tsconfig.json里至少开启这些配置{ compilerOptions: { target: ES2022, module: NodeNext, moduleResolution: NodeNext, strict: true, noImplicitAny: true, strictNullChecks: true, noUncheckedIndexedAccess: true, exactOptionalPropertyTypes: true, esModuleInterop: true, skipLibCheck: true, forceConsistentCasingInFileNames: true, outDir: ./dist, rootDir: ./src, declaration: true, declarationMap: true, sourceMap: true }, include: [src/**/*] }这里我特别提一下exactOptionalPropertyTypes这是很多人容易忽略的一个配置。默认情况下TypeScript 允许{ optionalProp: undefined }这种赋值这在普通应用中问题不大但在 MCP 工具定义中如果某个可选属性被显式设为undefined传输层可能会把这个字段当成存在但无效导致协议层报错。开启这个配置后类型系统会强制你正确地处理可选属性从源头上规避这类问题。4.2 构建产物exports 字段决定成败TypeScript 编译只是第一步更关键的是package.json里的exports字段。MCP 客户端在加载你的 Server 时会根据exports找到正确的入口文件。{ name: my-mcp-server, type: module, bin: { my-mcp-server: ./dist/index.js }, exports: { .: { types: ./dist/index.d.ts, import: ./dist/index.js } }, files: [dist], scripts: { build: tsc, dev: tsx watch src/index.ts, start: node dist/index.js } }很多人在本地开发时用tsx直接跑源码一切正常发布到生产环境后却出现“模块找不到”或“入口文件不存在”的报错大概率就是exports字段没配好或者files字段没把dist目录包含进去。另外一个常见坑是type: module。现代 Node.js 支持 ESM 之后MCP SDK 的推荐用法也是 ESM。如果你在package.json里没有设置type: module却又在源码里使用了import语法编译后的.js文件会被 Node 当成 CommonJS 解析直接报语法错误。这个坑我踩过一行配置花了我半天时间排查。4.3 环境变量管理别让配置散落一地MCP Server 一旦部署到不同环境本地、测试、生产环境变量的管理就成了一个容易被忽视的问题。我见过太多代码直接写process.env.API_KEY散落在各个文件里改一个配置要全局搜索。我的做法是在src/lib/config.ts里统一管理import { z } from zod; const configSchema z.object({ logLevel: z.string().default(info), apiKey: z.string().optional(), databaseUrl: z.string().default(sqlite:./data.db), maxTaskDurationMs: z.number().default(30000), }); export type AppConfig z.infertypeof configSchema; export function loadConfig(env: NodeJS.ProcessEnv process.env): AppConfig { const parsed configSchema.safeParse({ logLevel: env.LOG_LEVEL, apiKey: env.API_KEY, databaseUrl: env.DATABASE_URL, maxTaskDurationMs: env.MAX_TASK_DURATION_MS ? parseInt(env.MAX_TASK_DURATION_MS, 10) : undefined, }); if (!parsed.success) { throw new Error(配置校验失败: ${parsed.error.message}); } return parsed.data; }用 zod 做配置校验的好处是如果环境变量缺失或类型不对启动阶段就报错而不是等到运行时才出问题。这种“快速失败”的哲学在服务类应用里非常重要。4.4 运行时健康检查部署后的第一道防线MCP Server 对外是常驻服务必须提供健康检查能力。你可以注册一个health_check工具返回服务的存活状态、版本号、资源占用等信息。const healthCheckTool { name: health_check, description: 检查服务健康状态返回版本号和基本资源信息, inputSchema: { type: object, properties: {}, }, execute: async () { const memoryUsage process.memoryUsage(); return { content: [ { type: text, text: JSON.stringify({ status: healthy, version: 1.0.0, uptimeSeconds: process.uptime(), memoryMB: Math.round(memoryUsage.rss / 1024 / 1024), }), }, ], }; }, };这个工具在本地开发时看似没用部署后却是排查问题的利器。我遇到过部署完的新版本服务一直报错但日志又没输出有效信息的诡异情况靠着一个health_check工具确认了进程确实活着、能接请求才把排查方向转到业务逻辑上。5. 部署上线从本地到公网传输方式是第一道分水岭5.1 两种传输方式stdio 与 HTTP/SSEMCP 的传输方式直接决定部署策略。特性stdio 方式HTTP/SSE 方式适用场景本地开发、单机使用远程服务、多人共享客户端配置填写启动命令npx/node填写服务 URL工作方式客户端拉起子进程并通信客户端通过 HTTP 请求调用优点简单直观、权限天然隔离支持远程访问、可横向扩展缺点只能本机使用需要处理网络、认证、安全很多人的困惑是本地用 stdio 开发得好好的部署到服务器后在客户端配置里填了服务器地址却不生效。原因是你的 Server 根本没有启动 HTTP/SSE 模式的监听服务。我建议的开发部署路径是本地开发阶段用 stdio 方式配合modelcontextprotocol/inspector调试快速迭代业务逻辑。服务部署阶段用SSEServerTransport或StreamableHTTPServerTransport改为 HTTP/SSE 模式暴露公网地址。客户端接入将连接方式从“命令启动”改为“远程 URL”。5.2 远程部署实操反代 守护 安全下面是我实际部署一个生产级 MCP Server 时的完整链路第一步改造启动代码将服务从 stdio 模式改为 HTTP/SSE 模式。入口代码基本长这样import express from express; import { Server } from modelcontextprotocol/sdk/server/index.js; import { StreamableHTTPServerTransport } from modelcontextprotocol/sdk/server/streamableHttp.js; const app express(); app.use(express.json()); const server new Server( { name: my-mcp-server, version: 1.0.0, }, { capabilities: { tools: {}, resources: {}, }, } ); app.post(/mcp, async (req, res) { const transport new StreamableHTTPServerTransport({ sessionIdGenerator: undefined, }); res.on(close, () { transport.close(); }); await server.connect(transport); await transport.handleRequest(req, res); }); app.listen(3000, () { console.log(MCP Server listening on port 3000); });注意这里只是一个最小示例真实生产环境还需要处理会话管理和多请求路由建议以 MCP SDK 官方文档中的 HTTP 传输实现为准。核心就是收到 HTTP 请求建立传输通道接入 MCP Server 实例。第二步配置 Nginx 反代和 HTTPS强烈建议用 Nginx 做反向代理承担 TLS 终止、负载均衡、访问控制等职责。MCP 客户端通过公网访问时要求使用https协议证书可以免费申请并自动续期。server { listen 443 ssl; server_name mcp.example.com; ssl_certificate /etc/nginx/certs/fullchain.pem; ssl_certificate_key /etc/nginx/certs/privkey.pem; location /mcp { proxy_pass http://127.0.0.1:3000; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_read_timeout 300s; } }我这里把proxy_read_timeout设置为 300 秒是因为 MCP 工具调用可能会有较长的处理时间默认的 60 秒可能不够。第三步进程守护与日志管理目前我用得很顺手的是 PM2配置一个ecosystem.config.cjsmodule.exports { apps: [ { name: mcp-server, script: dist/index.js, instances: 1, autorestart: true, max_memory_restart: 500M, env: { NODE_ENV: production, LOG_LEVEL: info, }, out_file: /var/log/mcp-server/out.log, error_file: /var/log/mcp-server/error.log, merge_logs: true, kill_timeout: 5000, }, ], };这个kill_timeout: 5000是我加上的一个关键配置。MCP Server 在收到关闭信号时需要时间优雅地断开现有连接、清理资源。如果设置太短PM2 会强制杀掉进程正在处理的请求就会中断。5.3 实际案例设计稿 MCP 服务的接入方式最近注意到像蓝湖 MCP 这类产品很火本质上解决的是“让 AI 读取设计稿标注和数据结构”的需求。这类服务有几个明显特点需要访问云端设计数据适合做远程 HTTP/SSE 服务。涉及鉴权用户在客户端配置 token 或 API KeyServer 侧要做令牌透传和权限校验。设计稿数据量可能很大单次返回全部信息不现实需要支持按需查询、分批获取。在设计此类 MCP Server 时我会把工具划分得非常细比如list_projects列出用户可访问的设计项目。get_project_info获取项目基本信息。get_layer_tree获取指定页面的图层树结构。get_layer_detail获取单个图层的详细标注数据。这样的设计思路是模型可以根据用户需求先获取项目列表再一层层下钻避免一次性拉取过量数据。这也和前面讲的“流式输出设计”理念互通——把大任务拆成多个小任务每个小任务都快速返回。5.4 部署后的常见问题速查现象最可能的原因排查方法客户端连接远程服务失败未配置 HTTPS 或端口未开放检查网络安全组确认服务 URL 是否以 https 开头访问时提示 404Nginx 路径转发配置错误核对 location 路径和 Server 实际路由是否一致工具调用超时反向代理超时时间太短调大proxy_read_timeout进程频繁重启内存超限或未捕获异常查看 PM2 日志检查max_memory_restart设置多个客户端互相干扰未做会话隔离检查是否有共享的全局状态或在 HTTP 传输中合理管理会话6. 核心流程参考一个有完整骨架的 MCP Server 实现前面讲了很多理论和经验最后给一个可以直接“抄作业”的代码骨架把错误处理、流式输出、工具注册、配置管理都串起来。// src/index.ts import { Server } from modelcontextprotocol/sdk/server/index.js; import { StdioServerTransport } from modelcontextprotocol/sdk/server/stdio.js; import { withErrorHandling } from ./lib/error-handler.js; import { loadConfig } from ./lib/config.js; const config loadConfig(); const server new Server( { name: my-mcp-server, version: 1.0.0, }, { capabilities: { tools: { deep_search: { handleWith: async (args, emit) {...} }, health_check: { handleWith: async () {...} }, }, }, } );这里需要注意不同版本的 MCP SDK 对工具注册的 API 形态会有调整上面的handleWith写法只是示意实际开发时请参考你所使用版本的类型定义。重点是你已经看到标准的结构工具定义、参数校验、错误处理、业务实现、结果返回。如果你用的是 TypeScriptSDK 会提供完整的类型推导。工具的inputSchema建议通过 zod 推理生成而不是手工写 JSON Schemaimport { z } from zod; const DeepSearchSchema z.object({ query: z.string().describe(搜索关键词), limit: z.number().optional().default(10).describe(返回结果数量上限), }); type DeepSearchArgs z.infertypeof DeepSearchSchema;使用 zod 的好处是类型、默认值、描述三合一既能得到运行时校验又能在编译期获得完整的类型支持还能在生成inputSchema时自动带上各字段的描述信息帮助大模型正确理解并调用你的工具。结尾我自己的一些体会做 MCP Server 开发这段时间最大的感受是这个领域的技术栈并不复杂真正的难点在于“以生产级标准要求自己”。错误处理的规范、流式输出的设计、TypeScript 的严格配置、部署时对各种细节的把控每一项单独拎出来都不难难的是把它们组合在一起形成一个稳定、可靠、可维护的系统。如果你也在写 MCP Server我的建议是从第一天就把错误处理框架搭好哪怕只是一个小工具第一次做耗时任务时就想清楚流式输出的方案TypeScript 严格模式不要妥协部署时优先考虑 HTTP/SSE 远程模式因为这才是 MCP 服务真正的价值放大器——让不同的人、不同的 AI 应用都能通过标准协议使用你的能力。踩过几次坑之后你会发现MCP Server 的开发其实像搭积木框架搭对了后面每加一个新工具都很快框架搭错了每加一个功能都要回来补债。希望这篇文章能帮你少走一些弯路。