ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

MCP自定义服务器开发实战:从错误处理到生产部署全指南

MCP自定义服务器开发实战:从错误处理到生产部署全指南 1. MCP 自定义服务器到底是什么1.1 先搞清楚 MCP 在 Agent 世界里扮演的角色MCPModel Context Protocol是 Anthropic 在 2024 年底开源的一套标准化协议它的定位非常像 AI 世界的 USB-C 接口。你不需要为每一个硬件厂商定制一根专属充电线USB-C 统一了物理层和协议层MCP 则统一了 AI 应用也就是 Host与外部工具、数据源之间的通信方式。只要你的工具实现了 MCP Server任何支持 MCP 的客户端Claude Desktop、Cursor、自研 Agent、Codex 等都可以直接调用。这套协议的核心是三个概念Host 是发起方也就是你正在使用的 AI 应用Server 是能力提供方负责暴露工具、资源、提示词Client 则是协议层的桥接器负责在 Host 和 Server 之间建立会话。实际开发时你写的自定义服务器就是 Server 那一侧但你必须同时理解 Client 的行为方式否则你返回的数据格式稍有问题客户端那边解析不出来整个调用链路就直接断掉甚至不报错只是静默失败。1.2 为什么需要自己写 MCP Server而不是等官方适配很多人会问官方不是已经提供了一堆现成服务器吗文件系统、数据库、GitHub、Slack 都有。我的回答是现成服务器解决的是通用场景真实业务中至少有三种情况必须自己动手。第一种是内部系统对接。公司里的权限系统、工单平台、监控告警中心这些都不会有官方 MCP 适配只有内部 API 文档和一个鉴权 Token这时候写一个薄薄的 MCP 封装层把内部 API 包装成工具暴露给 Agent是最省力的路径。第二种是领域逻辑复杂的工具。比如你有一堆 Python 脚本处理数据分析步骤有二十多步每步都可能失败你需要的是把这套流程封装成一个带状态校验的 MCP 工具而不是让大模型自己去拼装步骤。第三种是性能敏感场景。通用服务器为了兼容各种情况往往有额外的序列化和请求开销你自己写的服务器可以针对特定工具做优化比如批量查询、连接复用、缓存热点数据。我自己第一次动手写 MCP Server起因特别朴素我本地有一套自动化测试平台想让它能被 Agent 直接触发但市面上没有现成适配。硬着头皮把协议读了一遍发现核心机制并不复杂真正的难度全在错误处理、流式输出、类型定义和部署这些“后期打磨”上这正好就是这篇文章要展开的内容。2. 开发前的准备技术选型与项目骨架搭建2.1 为什么我选了 TypeScript 而不是 Python当前 MCP 官方 SDK 主要有 Python 和 TypeScript 两个版本我最终选了 TypeScript原因是多方面的。首先是生态契合度我的团队后端主要是 Node.js已有的内部包管理、日志框架、部署流水线都能直接复用不需要额外引入 Python 运行时。其次是类型安全MCP 协议是基于 JSON-RPC 2.0 的请求和响应本质都是结构化 JSONTypeScript 可以在编译期就把参数结构、返回结构定死避免运行时才发现字段名拼写错误。第三是部署灵活Node.js 的产物可以打包成单文件或轻量 Docker 镜像冷启动速度比 Python 快不少agent 调用工具时每次拉起进程的等待时间能明显缩短。如果你是从零开始、团队没有历史包袱我其实也建议优先选 TypeScript不是因为 Python 不行而是类型系统的约束在协议对接场景里太值钱了。协议这种东西错一个字段名就是静默失败类型系统能挡住一大半低级错误。2.2 初始化项目与安装依赖老规矩先把项目初始化做干净。我用的是 pnpmnpm 也行没那么讲究关键是锁文件要提交到 Git。mkdir my-mcp-server cd my-mcp-server pnpm init pnpm add modelcontextprotocol/sdk zod pnpm add -D typescript types/node tsxzod 不是可选项。MCP SDK 里定义工具输入结构的方式就是 zod schema它既可以做运行时校验又能自动推导出 TypeScript 类型一份定义两处使用非常高效。然后初始化 TypeScript 配置{ compilerOptions: { target: ES2022, module: NodeNext, moduleResolution: NodeNext, outDir: ./dist, rootDir: ./src, strict: true, esModuleInterop: true, skipLibCheck: true, forceConsistentCasingInFileNames: true }, include: [src/**/*] }注意module和moduleResolution必须配成NodeNext否则你在 Node 环境用 ESM 导入modelcontextprotocol/sdk时会碰到模块解析报错。这个坑我踩过明明依赖装好了运行却提示找不到模块排查半天发现是moduleResolution用了默认的node。2.3 最小可运行的 Server 长什么样我先放一个最小实现让心里有个底后面的内容都在这基础上扩充。import { McpServer } from modelcontextprotocol/sdk/server/mcp.js; import { StdioServerTransport } from modelcontextprotocol/sdk/server/stdio.js; import { z } from zod; const server new McpServer({ name: demo-server, version: 0.1.0, }); server.tool( add, 两数相加, { a: z.number(), b: z.number() }, async ({ a, b }) { return { content: [{ type: text, text: String(a b) }], }; } ); const transport new StdioServerTransport(); await server.connect(transport);这段代码做了一件事定义了一个叫add的工具输入两个数字返回它们的和。注意connect之后程序不能退出它会在标准输入输出上持续监听请求。这里值得解释一下StdioServerTransport的设计意图。MCP 支持两种主要传输方式一种是 stdio也就是父进程把子进程的标准输入输出当作通信管道另一种是 HTTP/SSE通过 HTTP 请求进行通信。stdio 的好处是无需网络配置本地拉起一个 Node 进程就能跑特别适合 Claude Desktop 这类桌面客户端直接配置启动命令的场景。但 stdio 也意味着你的 server 生命周期完全由父进程掌控父进程退出你也就没了后面部署部分会专门讨论这个问题。3. 错误处理让你的 MCP Server 具备生产级稳定性3.1 MCP 协议里的错误模型和普通 HTTP API 完全不同写惯了 REST API 的人刚开始做 MCP Server最容易犯的错误是把 HTTP 状态码的思路带过来。HTTP 里你有 400、401、500 这类丰富的状态码客户端可以根据状态码决定重试还是降级。但 MCP 基于 JSON-RPC 2.0错误模型极其简单响应要么是成功的结果result要么是失败的错误对象error错误对象里包含code、message和可选的data。更关键的是MCP 的工具调用错误并不是直接抛出异常给大模型看的。你要知道调用链路上有一个大模型在“思考”大模型会根据工具返回的结果决定下一步动作。如果你的工具返回错误信息太含糊比如就是一个简单的 “Error occurred”大模型完全不知道发生了什么也不知道该怎么办它只能猜一猜就容易出错。3.2 实操在 TypeScript SDK 中抛出结构化错误SDK 内置了McpError类你需要学会正确使用它。看一个实际例子假设我们要写一个查询用户信息的工具用户不存在或者内部服务超时是两种完全不同的情况必须区分开。import { McpError, ErrorCode } from modelcontextprotocol/sdk/types.js; server.tool( get_user, 根据用户 ID 查询用户信息, { userId: z.string() }, async ({ userId }) { const user await db.query(SELECT * FROM users WHERE id ?, [userId]); if (!user) { throw new McpError( ErrorCode.InvalidParams, 用户 ${userId} 不存在请检查 ID 是否正确, { userId } ); } if (user.accountLocked) { throw new McpError( ErrorCode.InternalError, 用户 ${userId} 的账户已被锁定原因${user.lockReason} ); } return { content: [{ type: text, text: JSON.stringify(user) }], }; } );这里三个要素错误码、人类可读消息、附加数据。错误码让客户端程序做逻辑判断消息让大模型理解原因并调整策略数据则方便人工排查时拿到上下文。再强调一次message字段的服务对象首先是 AI。你不需要写“数据库查询语句存在语法错误SQLSTATE 42000...”你要写“查询用户信息失败因为用户不存在请检查传入的用户 ID 是否正确”。大模型看到这个消息后很大概率会主动纠正参数重新调用这比你返回一堆堆栈信息有用得多。3.3 三个级别的错误分开处理才是真的稳这是我做了几个 server 之后总结的分级策略按错误来源可以分成客户端错误、服务端错误、外部依赖错误三类每一类的处理策略完全不同。客户端错误参数格式不对、必填字段缺失直接用InvalidParams错误码抛出去不需要额外处理。大模型通常会自动根据错误信息修正参数你甚至可以故意在 message 里写清楚“正确格式是 xxx”帮助大模型自我纠正。服务端错误配置缺失、代码逻辑 bug、内部状态异常用InternalError抛出尽量附带足够上下文。这里一定要做日志记录因为服务端错误基本不会通过大模型自动恢复必须人工介入。外部依赖错误是最容易被忽视的。你的 server 调用了第三方 API第三方超时或者返回 429你不能简单把错误抛给大模型就完事。更合理的做法是捕获外部错误转成 MCP 错误并附上重试建议。比如“外部天气服务繁忙请 30 秒后重试”大模型会把这句话当作自己的判断依据可能真的等 30 秒再调用一次或者直接告诉用户服务暂时不可用。3.4 错误处理的核心心法给 AI 一条“下一步建议”这句话我想单独拿出来强调。你的错误消息里有没有“下一步建议”直接决定了大模型自动纠错能力的强弱。举一个我踩过的真实案例。我做了一个工具从数据库读取表结构。第一次测试时我传入了tableName但实际工具定义里参数名是table_namezod 校验直接失败。因为错误消息写的是 “Invalid params”大模型尝试了两次之后还是失败最后直接放弃了。我后来把错误消息改成 “参数异常正确的参数名是 table_name请传入正确的表名称”奇迹般地大模型下一次调用就完全正确了。所以写错误消息的通用公式是发生了什么 为什么发生 建议怎么做。这条公式不仅适用于 MCP Server所有面向 Agent 的工具开发都适用。4. 流式输出让长任务的用户体验从“卡死”变成“丝滑”4.1 流式输出在 MCP 里到底意味着什么很多人第一次接触“流式输出”头脑里想到的是 ChatGPT 网页上文字一个字一个字蹦出来的效果。但在 MCP Server 开发里流式输出的含义不完全一样。MCP 本身是 JSON-RPC 请求响应模型你调用一个工具最终拿到的是一个完整的 JSON 响应。如果你的工具执行需要 30 秒客户端那边的直观感受就是“卡住 30 秒然后一次性出结果”。这在本地脚本场景还能接受但如果你想做一个数据分析工具让大模型慢慢展示处理进度或者做一个报告生成工具让用户看到每一段的生成过程再或者做一个批量处理工具处理到一半用户就想取消那这种“全有或全无”的响应模型就非常糟糕。MCP 解决这个问题的方案是两类机制一类是通知Notification服务端可以在任务执行过程中主动向客户端推送进度更新另一类是读取流Read Stream服务端返回一个资源引用客户端通过后续请求持续获取结果。两者结合使用就能实现类似于“流式输出”的效果。4.2 实操用 Progress 通知上报阶段进度先看进度通知怎么实现。假设我们做一个批量处理工具处理 100 个文件需要两分钟。用进度通知用户可以实时看到“23/100”这样的进度。import { Server } from modelcontextprotocol/sdk/server/index.js; import { McpError, ErrorCode, ProgressToken, } from modelcontextprotocol/sdk/types.js; server.setRequestHandler( tools/call, async (request) { const { name, arguments: args } request.params; if (name ! batch_process) { throw new McpError(ErrorCode.MethodNotFound, 未知工具: ${name}); } const total args.totalFiles; const token request.params._meta?.progressToken; const sendProgress async (current: number) { if (token) { await server.notification({ method: notifications/progress, params: { progressToken: token, progress: current, total: total, message: 正在处理 ${current}/${total}, }, }); } }; for (let i 1; i total; i) { // 模拟耗时操作 await processFile(i); await sendProgress(i); } return { content: [{ type: text, text: 成功处理 ${total} 个文件 }], }; } );这里关键点是progressToken。客户端在发起工具调用时可以通过_meta.progressToken告诉服务端“我想接收进度通知”这个 token 是客户端生成的不透明字符串。服务端在发送通知时必须原样带回去客户端才能识别出这条通知属于哪一次工具调用。如果你的 Host 不支持这个字段token就是 undefined通知就直接跳过不影响主流程。经验之谈发送进度通知一定要有节流机制。如果你的循环处理速度极快比如每秒处理 100 条那每条都发通知会直接把进程 ping 死。我当时处理一个百万行数据处理任务就是加了“每处理 1000 条才发一次通知”的阈值性能瞬间正常。通知不是越频繁越好用户看进度也不需要精确到每一条。4.3 用“可读资源 轮询”实现真正的大结果流式返回进度通知能解决“过程可见”但如果工具要返回的数据本身非常大比如生成一个 10 万字的报告或者导出一个 500MB 的 CSV直接塞进 JSON 响应里既不现实也不优雅。这时候正确的姿势是工具先把结果写到临时文件或数据库然后返回一个资源链接客户端通过读取资源来获取完整结果。MCP 的 Resources 机制天然支持这个模式。看代码server.tool( analyze_large_dataset, 分析大规模数据集并返回结果下载链接, { datasetId: z.string() }, async ({ datasetId }) { const resultUrl /results/${datasetId}-${Date.now()}.json; // 异步开始分析直接把结果写入临时文件 // 这里假装已经完成写入了 await writeResultToTempFile(resultUrl, getDataset(datasetId)); return { content: [ { type: text, text: 分析已完成结果正在生成中..., }, ], resources: [ { uri: file://${resultUrl}, mimeType: application/json, name: 分析结果, }, ], }; } );当带resources返回时客户端会识别出这是可读取资源并通过resources/read请求来拉取内容。如果你的资源是动态生成的还可以配合notifications/resources/updated通知来告知客户端“内容更新了重新读取一下”。这个模式的优点是大结果不会阻塞 JSON-RPC 响应通道客户端可以自行决定何时读取、读取多少次。缺点是实现复杂度上去了你需要自己管理临时文件的创建、清理、过期策略。我的建议是临时文件一定要设置过期时间比如存储 30 分钟超过直接删除避免磁盘被撑爆。4.4 流式中断与超时兜底流式输出和长任务天然绑定那“用户取消”就一定是逃不开的话题。MCP 协议里有提供notifications/cancelled通知客户端可以发送这个通知来取消正在进行的任务。服务端要做的就是监听并响应。server.setRequestHandler( notifications/cancelled, async (request) { const { requestId } request.params; const task runningTasks.get(requestId); if (task) { task.abortController.abort(); runningTasks.delete(requestId); console.log(任务 ${requestId} 已取消); } } );每个任务启动时用AbortController包裹耗时操作取消时调abort()线程循环里检查signal.aborted来提前退出。这套机制在 Node.js 里很成熟但要注意取消不是免费的你在循环里必须主动检查中断信号否则外部取消根本不起作用。我在一次文件遍历任务里忘了加检查用户点取消后进程还在后台跑了十分钟后来加了if (signal.aborted) break;才好。再有就是服务端兜底超时。即使客户端没有取消你的任务也不能无限执行下去每类工具都应该有自己的超时阈值。比如内部 API 调用工具 10 秒超时批量数据处理工具 5 分钟超时超时后自动中断并返回错误信息。这个兜底意识必须有因为 AI 客户端的行为有时候是你预测不了的。5. 从开发机到生产环境部署方案与避坑指南5.1 stdio 模式的部署远没有想象中简单写完 server 之后本地npx tsx src/index.ts跑得很欢但一放到生产环境就各种问题。第一坑进程生命周期。stdio 模式的服务端是 “父进程拉起、父进程销毁” 的没有任何自愈能力。Claude Desktop 桌面应用如果崩溃了你的 server 进程也会变成孤儿进程。解决方案是用 systemd 或者 PM2 这类进程管理工具把我的 server 包成一个独立的长期运行进程。我不太喜欢直接在 Claude Desktop 的配置里写npx tsx来启动 server。因为每次启动都要走一份 TypeScript 编译慢而且可能被npx缓存坑到。正确做法是先把项目编译成 JS 产物然后用 systemd 管理一个独立的 Node 进程最后在客户端配置里指向编译后的入口文件。# /etc/systemd/system/my-mcp-server.service [Unit] DescriptionMy MCP Server Afternetwork.target [Service] Typesimple Userdeploy WorkingDirectory/opt/my-mcp-server ExecStart/usr/bin/node /opt/my-mcp-server/dist/index.js stdio Restartalways RestartSec3 EnvironmentNODE_ENVproduction [Install] WantedBymulti-user.target这个服务的重点在于Restartalways它保证进程挂掉后 3 秒内自动重启。但 stdio 模式有个麻烦systemd 默认接管标准输入输出如果配置错误server 的标准输入可能被 systemd 占用导致客户端无法通信。这个坑我踩过就是忘了设StandardInputnull和StandardOutputjournal。如果你在 systemd 下跑 stdio 模式的 MCP 服务一定要注意 input/ouput 的流向。5.2 Docker 镜像构建与连接类型的选型如果你有多个 Agent 应用需要访问同一个 serverstdio 模式就不太合适了因为每个客户端都要维护一个子进程资源消耗成倍增加。更好的做法是部署一个独立的长驻进程用 HTTP/SSE 方式暴露接口。MCP SDK 里有一个StreamableHTTPServerTransportstreamable HTTP新版默认推荐和老的SSEServerTransport这里我用 streamable HTTP 为例import { StreamableHTTPServerTransport } from modelcontextprotocol/sdk/server/streamableHttp.js; import express from express; const app express(); app.use(express.json()); app.get(/mcp, async (req, res) { const transport new StreamableHTTPServerTransport({ sessionIdGenerator: undefined, }); res.locals.transport transport; await server.connect(transport); }); app.post(/mcp, async (req, res) { const transport res.locals.transport; await transport.handleRequest(req, res); }); app.listen(3000);Docker 镜像的构建也比较套路化核心是多阶段构建。阶段一负责安装依赖并编译 TypeScript阶段二只复制编译后的产物和必要依赖这样镜像能小不少。FROM node:20-slim AS builder WORKDIR /app COPY package*.json ./ RUN npm ci COPY tsconfig.json ./ COPY src ./src RUN npm run build FROM node:20-slim WORKDIR /app ENV NODE_ENVproduction COPY package*.json ./ RUN npm ci --omitdev COPY --frombuilder /app/dist ./dist EXPOSE 3000 CMD [node, dist/index.js, http]多阶段构建的收益相当可观我同一套代码用单阶段构建镜像要 1.2GB去掉 dev 依赖和中间产物之后直接缩到 180MB 左右。镜像越小拉取越快启动越快Agent 调用工具的等待时间就越短。5.3 部署时最容易忽略的问题部署阶段有三个问题我几乎每次都会遇到值得单独列出来。第一个是环境变量管理。你的 server 一定有各种密钥数据库密码、API Token、服务账号千万别硬编码在代码里也别塞进 Docker 镜像层。用环境变量注入并在启动脚本里做显式校验缺了就快速失败。我在本地写过一个工具漏配了环境变量服务照常启动但每次调用工具都报错排查起来很恼火。后来我在启动入口加了一段专门的校验代码五个环境变量逐个检查缺一个就打印清晰提示并退出之后类似的低级问题就再也没有了。第二个是两个传输模式千万别混用。一个 server 实例要么用 stdio transport要么用 HTTP transport不能既在启动时 new 一个StdioServerTransport又在收到 HTTP 请求时 new 一个StreamableHTTPServerTransport。你可能会觉得“这不是废话吗”但我真见过有人把两种连接方式写在同一个进程里然后奇怪为什么客户端连不上。SDK 的底层连接状态管理是冲突的一个进程只能绑定一种传输方式。第三个是超时与反向代理。如果 server 跑在 Nginx 后面默认proxy_read_timeout只有 60 秒长任务比如批量处理 5 分钟会直接被 Nginx 切掉连接。我当时处理一个报表工具就是加了一行proxy_read_timeout 600s;才彻底解决。这个细节很容易被忽略但影响很致命本地直连没问题一上代理就超时。5.4 日志、监控与安全兜底生产环境的另一个要点是日志。你的 server 被大模型当工具调用问题排查难度比普通 API 大得多因为外层还有一层“AI 的思考过程”。我的做法是每次工具调用都记录结构化日志至少包含时间戳、请求 ID、工具名称、入参摘要敏感字段脱敏、出参摘要、耗时、错误信息。这些日志放到 ELK 或 Grafana Loki 里统一查询问题排查效率瞬间翻倍。安全方面也要注意。暴露在公网的 HTTP 模式的 MCP Server至少要做两层防护一层是 Bearer Token 鉴权请求头里必须带正确的Authorization另一层是指定可信任来源。MCP 是一个很新的协议生态里的安全实践还在快速演进中别把你的 server 裸奔到公网。另外注意入参校验zod 已经帮你做了类型校验但还有一层值得考虑的是业务权限校验。很多工具开发者的实现思路是“只要参数合法就放行”但一个工具能查询用户信息、操作文件、写数据库大模型一旦被恶意引导调用风险非常大。我的建议是每个工具都要考虑授权边界这个工具是被动的谁发起的请求它就执行你必须在工具层面对身份和权限做显式判断。6. 常见问题与排查技巧实录6.1 客户端连接失败与 stdio 进程 ID 排查典型报错“MCP error -32603: Internal error”。这种情况我们先分领域本地 stdio 模式连接失败大概率是启动命令写错了。检查客户端配置里的命令是不是node dist/index.js工作目录是不是项目根目录Node 版本是不是服务器要求的版本。还有一个小细节很多 server 用的tsx或ts-node在调试时能跑但客户端不会自动装依赖所以必须先用npm run build编译出 JS 产物再指向dist目录下的文件。我遇到过不下十次用户反馈“明明本地能跑客户端就是连不上”最后都是这个原因。如果你用 systemd 管理 server排查时就多一步先确认进程在跑systemctl status my-mcp-server再确认客户端配置的启动命令能直接执行比如手动跑一次node dist/index.js stdio看看是不是立刻退出。如果手动执行正常但 systemd 下不行多半是 systemd 服务文件里的WorkingDirectory或者Environment配置不对。6.2 返回值无法被客户端解析这种情况最常见大模型那边看不到工具结果或者看到的结果是一堆奇怪的 JSON。我这里的排查思路是这样的先用JSON.stringify把返回的对象打印到日志里确认 content 数组里的对象结构是不是{ type: text, text: ... }。MCP 的 content 对象类型有很多种文本类型是最通用的结构必须精确。我发现很多新手会漏掉type字段或者直接把字符串当成 content 数组用SDS 就直接报错。再一个常见问题是返回内容里包含了非常大而且无用的东西。有些工具把整个数据库表都打印出来塞进 content大模型的处理窗口有限很容易看到一半就截断然后告诉你“结果不完整”。这时的正确做法是返回核心摘要然后提供一个读取完整数据的资源引用。6.3 流式输出不生效的排查路径如果你加了进度通知但客户端始终不显示进度先确认你的 Host 是否支持这个协议特性。是的MCP 协议定义了进度通知但并不是每个客户端都实现了。Claude Desktop、Cursor、自研 Agent 对 MCP 规范的完整度差异很大很多实现可能还停留在最基础的“工具调用、拿结果”阶段。怎么判断最简单的方法是用 SDK 自带的测试客户端或者直接写一个简单的 Node 脚本发起工具调用检查控制台能不能收到notifications/progress消息。如果能收到说明你的 server 没问题问题在客户端侧。另一个容易忽略的点是进度通知里的progressToken必须和请求里的_meta.progressToken保持一致这个 token 不是你自己生成的是客户端在下发请求时传给你的你只能原样返回。6.4 TypeScript 版本和类型定义的兼容性最后说一下 TypeScript 本身。MCP SDK 更新速度很快版本之间可能会有 breaking change但你可能在 GitHub 上搜到的是旧版教程。我们团队就遇到过modelcontextprotocol/sdk从 0.x 升到 1.x 时McpServer构造器的 Options 类型变了zod的定义方式也略有调整。解决方案就是遇到类型报错先看 changelog别急着硬刚。另外推荐在项目里启用strict: true虽然刚开始开发时会各种报错但长期来看省下的排查时间绝对超过多写的类型标注。我自己在项目后期给所有工具的入参都补了详尽的 zod schema 注释后面再维护起来非常轻松——IDE 的智能提示直接告诉你每个参数的含义和格式完全不用翻代码找上下文。7. 一点个人建议这套开发流程走下来我自己最大的感受是MCP Server 的代码量并不大核心逻辑可能几百行就完了真正的功力全在那些不显山不露水的细节里——错误消息有没有给 AI 指路、大结果有没有合理分流、进程有没有托管重启、日志能不能追踪全链路。你考虑得越细你的工具被大模型正确调用的概率就越高用户的整体体验就越好。给正准备上手的读者一个建议不要一上来就追求功能复杂先把我上面说到的“最小实现”跑通然后逐项加上错误处理、进度通知、部署脚本。每一步都踩实了你对 MCP 协议的理解就不再是纸面上的概念而是肌肉记忆层面的工程能力。最后再分享一个小技巧写 server 的过程中多站在“大模型”的角度思考。你的工具是给 AI 用的AI 看到的只有函数名、参数描述和返回结果。你能不能让一个完全不了解背景的人AI仅凭这些信息就正确使用你的工具如果能你的 MCP Server 就是合格的如果不行继续打磨文档和错误提示这比优化代码性能更有价值。
返回列表