资讯动态

MCP协议实战:从零开发第一个MCP Server,让Agent调用你的服务

发布时间:2026/10/22 2:51:40 来源:尧图企业网站定制
最近连续接了几个项目开场白几乎一模一样能不能让 AI 帮我们操作一下内部系统需求一听就懂一做就卡住卡就卡在模型根本没有一条标准的路去调用我那一堆内部服务。早期各家都在做自己的工具调用格式接入一个系统就要写一套适配层换一个模型又得改一遍整个行业像是回到了每个手机品牌各自带一种充电口的时代。MCPModel Context Protocol模型上下文协议就是在这个背景下冒出来的它想做 AI 应用界的 USB-C。这篇文章我打算从协议本身的定位讲起再讲清楚 Tool、Resource、Prompt 三个核心抽象的区别然后带大家从一个真实的代码统计工具入手完整走一遍 MCP Server 的开发、调试和部署流程。内容主要面向准备把内部能力开放给 LLM 的后端工程师以及想自己做 Agent 工具的独立开发者如果你只是听过 MCP 但还没动手写过 Server这篇文章可以当作你的第一份实践地图。踩坑部分会写得比较细因为这些坑我基本都踩过能帮你省下不少时间。1. MCP 到底解决了什么问题先看一张角色分工图1.1 一个标准 MCP 架构里都有谁在写代码之前得先把 MCP 架构里的三个角色分清楚。MCP 把一次 AI 应用调用外部工具的过程拆成了三层最外层叫MCP Host就是用户直接面对的那个应用比如桌面客户端、网页应用、IDE 插件Host 内部会嵌入一个MCP ClientClient 负责和具体的MCP Server通信Server 是真正做事的一方它暴露出一系列能力给模型使用。还有第四个隐层角色是模型本身它不直接连 Server而是通过 Client 间接发现和调用工具。你可以这样理解Host 是老板Client 是秘书Server 是各个业务部门。老板不会自己跑到财务系统里拉数据他让秘书去对接财务部的接口秘书负责传话、翻译、把结果整理好拿回来。MCP 协议就是这一整套“职场沟通规则”定义好了消息长什么样、该怎么传递、出错怎么反馈。1.2 为什么底层要用 JSON-RPC 2.0MCP 的消息格式选的是 JSON-RPC 2.0没有走 REST 那套。刚开始我也觉得奇怪都这个年代了为什么不用 OpenAPI 那种风格后来实际写 Server 就明白了模型和 Server 之间的交互本质是双向的、异步的、带状态的REST 的资源语义在这里反而不合适。JSON-RPC 的消息结构非常简单一个请求就是 method、params、id 三个字段不需要为 CURD 设计一堆状态码和资源路径非常契合工具调用的心智模型。另一个好处是JSON-RPC 天然支持并发和消息关联。一次会话里可以同时发出多个 tools/call 请求每个请求带上不同的 id响应回来时根据 id 就能知道对应的是哪次调用。这在 MCP 里非常重要因为 Agent 场景经常需要模型一次性并行调用多个工具比如既查天气又查日历然后合并结果做决策。1.3 MCP 不解决什么和团队聊 MCP 的时候大家容易把它的能力边界想得太大。这里得把话说清楚MCP 不解决权限问题Server 暴露出去的 Tool 默认就是“模型可以调用”的具体谁能用、可用几次是 Host 和 Server 自己要在外层做鉴权的。MCP 也不解决模型记忆和状态管理每次工具调用的上下文除非你自己设计持久化否则调用完就结束了。MCP 更不解决 Agent 的规划能力它只是提供了“干活的手”大脑怎么想、怎么编排任务是模型和上层应用的事。认清边界之后你才不会被一些不切实际的需求带偏比如有人问“MCP 能不能帮 Agent 自动扩容服务器”能是能但那个“自动”后面挂的权限和审计责任全部要由你的 Server 自己扛。协议只是管道安全是管道两侧的人的事。2. 三种能力抽象Tool、Resource、Prompt别一上来全塞成 Tool2.1 Tool模型主动发起的“动作”Tool 是 MCP Server 里最核心、也最常用的一种能力。它的语义是模型根据用户的意图决定要不要调用某个工具、传什么参数。一次完整调用流程是Client 在初始化和能力协商阶段拿到 Server 的 tools/list 列表模型看到列表后根据当前对话决定调用哪个工具发一个 tools/call 请求Server 执行并返回结构化结果。Tool 适合做有副作用的操作查询订单、写数据库、发消息、执行命令都属于这一挂。每个 Tool 必须定义清晰的 inputSchema这个 schema 就是模型理解工具用的说明书写得好不好直接决定模型调用得准不准。后面我单独有一节专门讲 schema 设计。2.2 Resource让 Server 去“读”数据Resource 解决的问题是“模型怎么拿到一块数据”。它代表可被读取的数据源比如一个文件、一张表、一个 URL 对应的内容。模型通过 resources/read 请求读取资源内容客户端也可以把 Resource 列表展示给用户让用户手动“授权”或“拉取”某个资源进对话。什么时候该用 Resource 而不是 Tool我的判断标准很简单如果这个操作主要是“拿数据回来给模型看”没有明显的副作用就适合做成 Resource如果这个操作会改变外部系统状态比如删库、写文件那必须走 Tool。Resource 的 URI 命名也有讲究类似 file:///logs/app.log、db://orders/open见名知意方便模型理解。2.3 Prompt固定的“话术模板”Prompt 抽象经常被忽略实际上它非常有用。它的语义是Server 侧提供一些可复用的提示词模板客户端可以把这些模板和用户输入拼装后发给模型实现一种“预置Prompt注入”的效果。比如你做一个客服系统 Server可以内置一个“工单分类”的 Prompt自动带上分类规则和样例用户在客户端选择这个模板模型就以特定方式处理当前工单。我的经验是Prompt 更适合做领域专家知识的沉淀。把它放在 Server 里应用侧不用重复维护提示词多个 Host 接入同一套领域逻辑时也保持了一致性。2.4 三种能力混用的典型场景实际项目里这三种能力往往配合使用。比如我做的项目监控工具代码仓库的读取用 Resource读文件、读 diff扫描和分析动作用 Tool执行 git log、统计行数最后生成周报的格式用 Prompt 定义。这么拆的好处是每一块都职责单一模型在不同阶段按需取用调试也更方便。要是图省事把所有东西都塞进 ToolServer 会变成一个大杂烩模型的理解成本和误调率都会上升。3. 动手开发第一个 MCP Server用 TypeScript 做一个“代码体检”工具3.1 选型为什么我推荐 TypeScript SDKMCP 官方提供了 TypeScript、Python、Java/Kotlin 等多个语言的 SDK我主力用的是 TypeScript 版本。理由有三第一MCP 生态的客户端、调试工具对 TypeScript 支持最好遇到问题社区参考最多第二TypeScript 的 zod 库写 schema 特别顺手类型安全直接贯穿到协议层第三大部分做 Agent 应用的团队本来就在 Node 技术栈里维护成本低。Python SDK 也很成熟如果你的逻辑偏数据计算和 AI选 Python 没问题。我遇到过一些 Java 团队把 MCP Server 嵌进现有微服务里用官方 Java SDK也没毛病。选型核心就看一件事哪个语言里你的业务逻辑和基础设施代码最成熟就让 Server 离它们最近。3.2 初始化工程和最小依赖先拉一个空项目安装依赖。这是我最常用的一套组合npm init -y npm install modelcontextprotocol/sdk zod npm install -D typescript tsx注意MCP SDK 现在的版本迭代比较快API 形态可能有调整装完以后打开 node_modules 里的类型定义瞄一眼确认 registerTool 或 tool 方法的具体签名。我下面给的是当时验证过的写法你以官方文档为准。TS 配置里记得开module: NodeNext或者直接用 ESM不然 import 语法会报错。3.3 先理解 Server 与 Client 的握手流程写第一个 Server 之前先背一下协议里的时序这是最容易出问题的区域。MCP Server 启动后Client 会按顺序发来 initialize 请求双方交换协议版本和能力声明接着 Client 发送 tools/list拿到全部工具列表用户让模型干活时模型通过 Client 发来 tools/callServer 执行并返回结果。整个过程是 JSON-RPC 消息在传输层上跑消息边界清晰但时序不能乱。如果你用官方 SDK这些是封装好的你要的只是“说清楚我的工具是什么、参数是什么、执行函数是什么”。下面这段代码是核心骨架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: code-insight, version: 0.1.0, }); server.registerTool( scan_code_stats, { title: 统计代码仓库体检指标, description: 扫描指定目录下的代码文件返回总行数、空行数、注释行数以及 TODO/FIXME 标记数量适合在评审前快速了解一个仓库的规模与健康度。, inputSchema: { rootPath: z.string().describe(要扫描的仓库绝对路径), extensions: z .array(z.string()) .optional() .describe(文件扩展名白名单默认 [.ts, .js, .py]), }, }, async (args) { // 在这里写业务逻辑最终必须返回 content 数组 const result await scanRepo(args.rootPath, args.extensions); return { content: [{ type: text, text: JSON.stringify(result, null, 2) }], }; } ); // 启动通过 stdio 与 MCP Client 通信 const transport new StdioServerTransport(); await server.connect(transport);留意几个细节。inputSchema 不是给前端表单用的是给模型看的所以 description 一定要写清楚“这个参数取值范围是什么、默认行为是什么”。工具 description 也要具体模型就是靠它决定什么时候调用。返回值必须是 content 数组MCP 现在还支持图片和嵌入资源常用的就是 text 文本。3.4 扫描逻辑里的隐藏细节scanRepo 的实现看起来简单实际有几个小坑。第一个是文件遍历千万别用纯递归同步遍历大仓库会卡死用异步迭代器或者现成的遍历库。第二个是注释统计不同语言注释符号不一样简单的字符串匹配没法区分字符串里的//和注释里的//代码体检工具够用就行不必追求 AST 级精度但要在输出里标注“估算值”避免误导使用者。第三个是路径约束Server 可能跑在用户本机模型传进来的路径必须做正则校验和根目录约束否则一个恶意或错误的路径就能让 Agent 扫描整个磁盘这是我在安全审查里反复强调的一点。async function scanRepo(rootPath: string, extensions: string[] [.ts, .js, .py]) { const normalized path.resolve(rootPath); // 简单防越界检查实际可做更严格的目录白名单 const files await listFilesByExt(normalized, extensions); let lines 0, blanks 0, comments 0, todos 0; for (const file of files) { const text await readFileSafe(file); const fileLines text.split(\n); lines fileLines.length; for (const line of fileLines) { if (line.trim() ) blanks; else if (line.trim().startsWith(//) || line.trim().startsWith(#)) comments; if (/TODO|FIXME/i.test(line)) todos; } } return { rootPath, files: files.length, lines, blanks, comments, todos }; }这个工具麻雀虽小但覆盖了 MCP Server 开发的完整链路定义 schema、实现业务、注册工具、连接传输层。把它跑通你就已经跨过初学者到实践者的门槛了。4. Debug 才是真正的学习过程stdio 日志、Inspector 与三个棘手报错4.1 为什么在 stdio 模式下不能随便 console.log本地调试 MCP Server 时最常用的传输方式就是 stdio也就是父进程启动子进程通过标准输入输出传 JSON-RPC 消息。通道只有 stdout 一根它是传协议消息用的你在业务代码里 console.log 的任何东西都会污染协议流导致 Client 解析失败。很多新手在这个地方卡半小时症状就是 MCP Client 能启动但拿不到工具列表日志全部乱码。解决办法是日志一律走 stderr也就是 console.error或者接一个 debug 包输出到 stderr协议消息走 stdout两者互不干扰。我这个习惯现在是铁律任何 MCP Server 代码里不允许出现裸的 console.log。4.2 MCP Inspector调试器才是真正的工作台手动起一个 Client 去连 Server 太痛苦官方提供了 Inspector 这个调试利器。它可以看作是 MCP 的 Postman启动后会在浏览器面板里展示 Server 的所有 Tool、Resource、Prompt并且可以直接手动发 tools/call 测试。启动方式很简单npx modelcontextprotocol/inspector node dist/index.jsInspector 启动时自带一个 MCP Client与你本地 Server 建立连接你只管在页面上操作。我在开发期的主要工作流是改代码 → 重启 Inspector → 点几下工具测试 → 看返回结果和耗时。Inspector 还能查看协议原始报文这对排查“为什么 schema 没生效”特别有用。4.3 棘手的报错一stdio URL 前缀问题如果你把远程 HTTP 形式的 Server 地址直接写进了 stdio 配置MCP Client 会一直报“failed to connect”。这个问题的根因是配置语义stdio 模式的 command 只能填可执行程序和参数URL 是 streamable HTTP 模式用的。如果你确实要连一个远程 Server应该改走 HTTP 类型配置并且检查地址是不是以 http/https 开头。我当时排查这个问题很狼狈因为 Client 日志只提示连接失败不给具体原因最后是手动在终端里跑了一遍 Protocol 报文才发现前缀写错了。这里明确记一下本地进程就是command args远程服务就是url别混用。4.4 棘手的报错二CWD 不对导致工具全部找不到还有一种常见症状是 Server 启动成功但工具执行时相对路径的文件全部报不存在。问题不在工具逻辑在启动配置的工作目录。MCP Client 默认从配置文件所在的目录或系统默认目录启动子进程如果你的 Server 代码用相对路径读配置而 CWD 根本不指向项目目录自然找不到文件。解法有两个方向一是配置项里显式指定 command 和 args把工作目录当作最外层参数传进 Server二是在 Server 内部禁止使用相对路径所有资源路径基于 config 里显式声明的 basePath 拼接。后者更稳因为 Server 永远不知道它的 CWD 是什么。4.5 棘手的报错三JSON-RPC id 对应不上SDK 封装好的 Request 和 Response 不会出这个问题但你要是手写 JSON-RPC 客户端做联调就得小心 id 对应。MCP 允许多个请求并发响应消息里的 id 必须和请求一致Client 根据 id 匹配回调一旦 id 乱了Promise 就永远挂起工具调用看起来像卡死。联调经验把每个请求的 id 做成递增整数同时在日志里给每个请求标记一个业务名响应回来时对照看。如果响应里的 method 字段不该出现却出现了多半是消息格式混用了请求和通知两种形态。5. 部署形态选择本机 stdio 与远程 Streamable HTTP 的取舍5.1 场景决定协议手持端还是服务端MCP Server 跑在哪里基本决定了你选哪种传输方式。桌面应用和 IDE 插件这类部署在用户本机的 Client最适合 stdio 方式Client 启动本地子进程数据不离开本机延迟低不需要网络层鉴权配置也最简单。你只需要提供一个可执行入口用户把它写进 Client 的 MCP 配置文件里就行。但如果你要把一个统一的企业数据能力开放给多个 Agent 应用共用那就应该部署成远程服务。MCP 现在主推的是 Streamable HTTP 传输取代早期的 HTTPSSE 方案。它把 MCP 服务架成一个标准的 HTTP 端点Client 通过 POST 请求发送 JSON-RPC 消息服务端可以同步返回结果也可以在需要时用 stream 方式把多条消息推给客户端。这个模式下 CORS、鉴权、网关、超时全部变成常规 Web 工程问题你的既有基础设施能直接复用。5.2 远程 MCP 服务的四道必答题部署远程 Server 之后有几个问题必须在设计阶段就回答。第一是鉴权MCP 协议本身不带认证一般做法是在 HTTP 层挂 Token 或 mTLSClient 配置里带上 Authorization 头或 Token。第二是超时工具执行超过 HTTP 层默认 30 秒可能被网关掐断你要么把同步渲染改成 task 式先返回任务 ID再用单独的工具查结果要么允许 Server 发起 SSE 流持续推送。第三是限流和配额模型调用工具往往比人更频繁没有配额限制一个循环调用就能把后端打爆。第四是审计每次工具调用的入参、出参、耗时、调用方身份最好全部落日志必要时归档这不只是为了排查问题更是为了出事故时能讲清楚责任边界。这些都不是 MCP 协议给的是你这个 Server 作者要自己背起来的。5.3 多 Server 管理进程还是容器如果一个 Host 要接多个 MCP Server本地场景就用进程管理工具做拉起、重启、日志拆分确保某个 Server 崩了不影响其他远程场景就做成独立服务或容器按各 Server 的负载独立扩容。我建议不要把业务逻辑全部堆进一个巨型 Server拆成职责单一的多个 Server用 Host 统一编排这才是 MCP 设计的预期用法。6. MCP Tool 接口设计心得好用与难用的分界线在哪里6.1 inputSchema 要像对外 API 一样认真对待我 Review 过不少人写的 Tool最大的通病是 schema 随便写description 模棱两可。记住一个事实模型不是人它不会“猜”你的参数含义你要把参数当作文档写给一个从没见过这个领域的聪明实习生看。默认值、合法范围、互相依赖的字段关系全部要在 describe 里说清楚。举个例子同一个时间字段写成“deadline: z.string()”模型大概率传 ISO 字符串的任意变体写成“deadline: z.string().describe(ISO 8601 格式例如 2025-07-01T10:00:00Z)”模型就会规规矩矩按格式传。经验法则增加一个参数解释的成本远低于模型传错参数后你的容错成本。6.2 错误返回要让模型“看得懂”Tool 执行失败时返回的错误信息必须面向模型下一步决策来写。不要只返回“ERROR: code 500”要写明“调用失败原因是数据库连接超时建议 5 秒后重试”这类结构化提示。模型拿到这个信息后往往会自动做一次重试或者换一种方案效果非常明显。同时Server 内部的异常不能让 JSON-RPC 层直接暴露堆栈要把堆栈记录在自己日志里给模型回传一个人能读懂的摘要。一个稳定 Server 的定义不是“永不返回错误”而是“错误信息能被模型正确消费”。6.3 危险动作要有“人在环内”的控制MCP 让模型能操纵真实系统之后危险动作的护栏就成了设计核心。我有几个固定策略第一破坏性 Tool 的 inputSchema 增加 confirm 字段必须显式传{ confirm: delete }才执行同时日志里记录调用者身份。第二把“查询”和“变更”拆成不同 Tool而不是一个大 Tool 里塞两个模式防止模型在只该读的时候误写了。第三涉及外部系统的操作优先让 Server 内部走审批接口而不是模型直接改库。这套东西不是协议强制要求的但你要是不做迟早会在某次误操作之后追悔莫及。MCP 出现本来就是为了让 AI 拥有更多能力能力越大接口设计的责任越大。6.4 幂等性和副作用管理写 Tool 时心里要始终记着“这个工具可能会被模型并行调用多次”。如果你的 Tool 是发起支付、发送邮件这类强副作用操作就必须在业务层做幂等控制让同一个业务 ID 对应的操作只能成功一次。模型自己是没有“只调用一次”的纪律的可能因为网络重试、Agent 循环而重复触发Server 必须兜住。我在代码体检工具里就故意把扫描设计成纯只读无状态稍微重跑几次也没关系一旦 Side Effect 出现复杂度立刻上了一个台阶。新手入门 MCP 开发建议先做这类只读工具把协议链路摸透再逐步接入驻户、消息、工单等有副作用的系统踩坑风险小很多。7. 最后一个是真金白银的经验MCP Server 的日常开发节奏上面把协议、代码、部署和设计都讲完了最后分享一套我自己的开发节奏适用于大多数中小型 MCP 项目。第一步直接在仓库里建一个mcp-server/子目录和主应用代码放在一起但独立发布因为 Server 的生命周期和主应用不一致可能要频繁单独上线。第二步开发库配置好tsx watch或者 nodemon改代码自动重启进程配合 Inspector 刷新测试这能显著提高迭代速度。第三步本地打通后第一时间写好一个只读的样例工具并拿到 Client 里跑通不要等所有工具写完再适配协议层没问题再批量加工具。第四步日志从第一天就走 JSON 结构输出到 stderr 文件方便后续做审计和分析。第五步每个工具写一个最小冒烟用例脚本直接拿 SDK 的 Client 库在测试里调一次保证 CI 里 MCP Server 不会静默坏掉。开发 MCP Server 本身不难难的是理解它解决的是连接问题而不是业务问题。所有真正复杂的决策——权限、安全、错误处理、部署方式——仍然是你自己的工程决策。协议给了你一个干净的接口规范剩下的还是要靠技术判断力去填。希望这篇文章能让你少走几段弯路真正把 Agent 的手接到你的系统上。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑