ARTICLE DETAIL

资讯详情

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

MCP协议实战:从零构建Agent看Bug的MCP Server

MCP协议实战:从零构建Agent看Bug的MCP Server 1. 从一个真实场景说起为什么我们需要 MCP上个月团队里有个刚转岗过来的后端同学问我说他写了个小工具想让本地的 AI 助手直接去读他项目里的日志文件、查数据库里的异常记录结果折腾了两天要么是权限报错要么是模型根本不知道该怎么调用他的函数。他问我有没有一种标准化的方式让 Agent 能像插 USB 一样接上我的工具这个问题其实问到了点子上。MCPModel Context Protocol就是干这个的。简单说它是一套让大模型 Agent 和外部工具、数据源之间对话的通用协议。你可以把它理解成 Agent 世界的 USB-C 接口——不管你是数据库、文件系统、还是某个内部 API只要按 MCP 的规范封装一次任何支持 MCP 的 Agent 都能直接调用不用为每个模型单独写适配层。我拿一个最朴素的场景来举例让 Agent 替你看 Bug。传统做法是你把报错日志复制粘贴给 AI它给你分析。但日志可能几百兆粘贴不现实而且你希望它能自己去翻代码、查最近的提交记录、甚至跑一下测试。这时候就需要 Agent 通过 MCP 去调用这些工具。本文就围绕这个场景把 MCP 服务的核心机制、stdio 传输方式、JSON 消息格式、以及一个能跑起来的最小示例讲透。适合已经用过 AI 编程助手、想进一步做 Agent 工具集成的开发者也适合刚接触 MCP 协议、想搞明白它到底怎么运转的同学。2. MCP 协议到底解决了什么问题2.1 没有 MCP 之前Agent 接工具有多痛在 MCP 出现之前让 Agent 调用外部能力基本有三种土办法。第一种是函数调用Function Calling每个模型厂商的格式都不一样OpenAI 一套、Anthropic 一套、国内几家又各有一套你写好的工具描述换个模型就得重写。第二种是提示词里塞工具说明让模型输出特定格式的文本你再解析这种方式极其脆弱模型稍微跑偏就解析失败。第三种是自己写胶水层针对每个模型、每个工具写适配代码维护成本高得离谱。我踩过最深的坑是同一个查数据库的工具在 A 模型上跑得好好的换到 B 模型后因为工具描述的 JSON Schema 字段名不兼容直接罢工。排查了半天才发现是参数命名风格的问题。这种重复劳动本质上是因为缺少一个中间协议层。2.2 MCP 的分层设计Host、Client、ServerMCP 把整个体系拆成三个角色这个划分非常关键理解了它后面看代码就顺了。Host宿主就是运行 Agent 的那个应用比如你的 IDE 插件、聊天客户端、或者自己写的 Agent 程序。它负责和用户交互决定什么时候该调用工具。Client客户端Host 内部为每个 Server 连接维护的一个客户端实例负责和 Server 通信把 Host 的意图翻译成 MCP 消息。Server服务端真正提供能力的一方比如一个文件读取服务、一个数据库查询服务。它对外暴露工具Tools、资源Resources、提示模板Prompts三类能力。这种设计的妙处在于解耦。Host 不需要知道 Server 内部怎么实现Server 也不需要知道 Host 是哪个模型。双方只认 MCP 协议这一套消息格式。你写一个查 Bug 日志的 MCP Server今天给这个 Agent 用明天换个 Agent 照样能用。2.3 三类核心能力Tools、Resources、Prompts很多人一开始分不清这三者我用看 Bug 的场景给你对应上。Tools工具是 Agent 可以主动调用的函数会产生副作用或执行动作。比如read_log_file、query_error_db、run_test。Agent 决定我要读这个文件就发起一次工具调用。Resources资源是 Agent 可以读取的数据通常是只读的、被动的。比如把某个日志文件作为一个资源暴露出去Agent 可以按 URI 去取内容。它更像给 Agent 看的资料而不是让 Agent执行的动作。Prompts提示模板是预定义的提示词模板Server 可以提供一些针对特定任务的提示Host 拿来直接用。比如一个分析堆栈异常的模板里面已经写好了分析框架。提示新手最容易混淆 Tools 和 Resources。记住一句话——Tools 是动词Resources 是名词。要执行动作就用 Tools要读取数据优先考虑 Resources。3. stdio 传输最简单也最常用的连接方式3.1 为什么示例首选 stdioMCP 支持多种传输方式最常见的是stdio标准输入输出和基于 HTTP 的传输。做示例、做本地工具我强烈建议从 stdio 入手原因有三。第一零网络配置。Host 直接以子进程方式启动 Server通过 stdin 发消息、stdout 收消息不需要开端口、不需要处理跨域、不需要证书。第二调试直观。你可以手动往 stdin 里敲 JSON看 stdout 返回什么排查问题非常方便。第三安全性好。进程间通信天然隔离不用担心端口暴露。代价是 stdio 只适合本地场景Server 必须和 Host 在同一台机器上。但对于让 Agent 看本地 Bug 日志这种需求本地恰恰是最合适的。3.2 stdio 通信的消息格式stdio 传输的核心规则很简单每条消息是一行 JSON以换行符分隔。注意不是整个流是一个 JSON而是每行一个独立的 JSON 对象。这一点非常关键很多新手把多个 JSON 拼在一起发导致解析失败。一个典型的请求消息长这样{jsonrpc:2.0,id:1,method:tools/list,params:{}}一个典型的响应消息长这样{jsonrpc:2.0,id:1,result:{tools:[{name:read_log_file,description:读取指定路径的日志文件,inputSchema:{type:object,properties:{path:{type:string}},required:[path]}}]}}可以看到它遵循JSON-RPC 2.0规范有jsonrpc版本号、id请求标识、method方法名、params参数、result结果。id用来把请求和响应配对因为 stdio 是异步的可能同时有多个请求在飞。3.3 一次完整的 stdio 握手流程Agent 启动一个 MCP Server 后不是上来就调工具而是要先握手。标准流程大致是Host 启动 Server 子进程。Host 发送initialize请求带上协议版本和客户端能力。Server 返回自己的能力和版本信息。Host 发送notifications/initialized通知表示初始化完成。之后 Host 可以发tools/list获取工具列表再发tools/call调用具体工具。这个流程看起来繁琐但它是保证双方能力协商一致的必要步骤。跳过握手直接调工具很多 Server 会直接拒绝。4. 手把手写一个看 Bug的 MCP Server4.1 环境准备与依赖选择我用 Python 来写这个示例因为它的 MCP 官方 SDK 比较成熟代码量少。你需要准备Python 3.10 及以上SDK 用到了较新的类型语法安装官方 SDKpip install mcp一个支持 MCP 的 Host比如某些 AI 编程客户端或者你自己写一个测试脚本选 Python 而不是 Node 或 Go纯粹是因为示例要短、要能一眼看懂。生产环境你完全可以用任何语言实现只要遵守 JSON-RPC 消息格式即可。4.2 定义工具读取日志与查询异常先想清楚我们要给 Agent 提供什么能力。围绕看 Bug我设计两个工具read_log_file读取指定路径的日志文件支持按行数截取避免一次读几百兆。search_error在日志里搜索包含指定关键字的行返回匹配结果和上下文。为什么拆成两个而不是一个因为 Agent 的调用是有成本的让它先搜索定位、再精确读取比一次性把整个文件塞给它要高效得多。这也是设计 MCP 工具时的一条经验工具要小而专让 Agent 自己组合。4.3 核心代码逐段拆解下面是最小可运行的服务端代码我逐段解释。import asyncio from mcp.server import Server from mcp.server.stdio import stdio_server from mcp.types import Tool, TextContent app Server(bug-inspector) app.list_tools() async def list_tools(): return [ Tool( nameread_log_file, description读取指定路径的日志文件可指定最大行数, inputSchema{ type: object, properties: { path: {type: string, description: 日志文件路径}, max_lines: {type: integer, description: 最多读取行数, default: 200} }, required: [path] } ), Tool( namesearch_error, description在日志文件中搜索包含关键字的行, inputSchema{ type: object, properties: { path: {type: string}, keyword: {type: string} }, required: [path, keyword] } ) ]这段代码做了两件事创建 Server 实例注册工具列表。注意inputSchema用的是标准 JSON Schema这是让模型理解参数的关键。描述写得越清楚模型调用越准。app.call_tool() async def call_tool(name: str, arguments: dict): if name read_log_file: path arguments[path] max_lines arguments.get(max_lines, 200) with open(path, r, encodingutf-8, errorsignore) as f: lines f.readlines()[:max_lines] return [TextContent(typetext, text.join(lines))] if name search_error: path arguments[path] keyword arguments[keyword] matched [] with open(path, r, encodingutf-8, errorsignore) as f: for i, line in enumerate(f): if keyword in line: matched.append(f{i}: {line.rstrip()}) return [TextContent(typetext, text\n.join(matched) or 未找到匹配内容)]call_tool是工具调用的入口根据name分发。返回的必须是TextContent列表这是 MCP 规定的返回格式。注意我加了errorsignore因为日志文件里经常有编码混乱的字符不加这个读取会直接抛异常。async def main(): async with stdio_server() as (read, write): await app.run(read, write, app.create_initialization_options()) if __name__ __main__: asyncio.run(main())最后这段是启动逻辑stdio_server把标准输入输出包装成读写流交给 Server 运行。整个服务端不到 60 行但已经是一个功能完整的 MCP Server 了。4.4 在 Host 里配置并验证Server 写好后需要在 Host 的配置里注册它。大多数 Host 用一份 JSON 配置文件格式类似{ mcpServers: { bug-inspector: { command: python, args: [/path/to/bug_server.py] } } }command是启动命令args是参数。Host 会按这个配置把 Server 作为子进程拉起来。配置完重启 Host如果一切正常你问 Agent帮我看看 /var/log/app.log 里有没有 error它就会自动调用search_error工具。注意路径一定要用绝对路径。我见过太多人写相对路径结果 Host 的工作目录和你想的不一样Server 启动就报文件找不到。5. 调试与排查那些文档不会告诉你的坑5.1 消息格式错误是最常见的翻车点stdio 传输对格式极其敏感。最常见的错误有三个一是忘了换行符把 JSON 发出去但没加\nServer 一直等下一行永远不响应二是一行里塞了多个 JSON解析器直接懵三是JSON 里有非法字符比如日志内容里带了未转义的控制字符。排查方法很土但很有效把 Server 的 stdout 重定向到文件看它到底输出了什么。如果输出是空的说明 Server 卡在读取阶段如果输出是乱码多半是编码问题。5.2 工具描述写不好模型就不会调这是最容易被忽视的一点。很多人代码写得没问题但 Agent 就是不调用工具或者调用时参数乱填。根因往往在description和inputSchema上。我的经验是描述要写清楚什么时候用和参数是什么。比如read_log_file的描述不要只写读取日志而要写读取指定路径的日志文件当需要查看日志具体内容时使用可指定最大行数避免读取过大文件。参数描述也要具体path要说明是绝对路径还是相对路径。5.3 常见问题速查表现象可能原因排查方向Server 启动后无响应未发送 initialize 或格式错误检查握手消息是否完整工具列表为空list_tools 未注册或返回格式错确认返回的是 Tool 对象列表调用工具报参数错误inputSchema 与实现不匹配对照 schema 检查必填字段中文乱码编码未指定读写文件统一用 utf-8读取大文件卡死未限制行数加 max_lines 参数并设默认值日志里有特殊字符导致解析失败JSON 转义问题用 SDK 的序列化而非手拼字符串5.4 几个我踩过的实操心得第一永远用 SDK 提供的序列化方法不要自己拼 JSON 字符串。日志内容里一个引号就能让你的手拼 JSON 崩掉SDK 会自动处理转义。第二给工具加超时和大小限制。Agent 可能让你读一个 2GB 的日志不加限制直接把内存吃满。我在read_log_file里默认限制 200 行就是这个原因。第三日志路径要做白名单校验。虽然本地场景风险低但如果 Agent 被诱导去读系统敏感文件还是会有问题。加一个允许的目录前缀检查几行代码的事。第四调试时先手动测。写个脚本直接往 Server 的 stdin 发tools/list看返回对不对比在 Host 里反复重启快得多。6. 从示例到生产MCP 服务的扩展思路6.1 把更多能力封装成工具示例里只有两个工具实际项目中你可以继续扩展。比如加一个git_recent_commits工具让 Agent 查最近的提交记录结合日志时间点定位是哪次改动引入的 Bug再加一个run_unit_test工具让 Agent 跑测试验证猜测。工具越多Agent 能做的事越多但也要注意别一次暴露太多工具模型的选择成本会上升一般控制在 10 个以内比较合适。6.2 资源与提示模板的配合使用除了 Tools你还可以把项目的README、架构文档作为 Resources 暴露出去让 Agent 在分析 Bug 时能参考背景知识。再提供一个堆栈异常分析的 Prompt 模板把分析框架固化下来。三者配合Agent 的分析质量会明显提升。6.3 安全边界与权限控制生产环境一定要考虑权限。我的做法是MCP Server 只暴露必要的目录和操作敏感操作比如删除文件、执行任意命令一律不封装成工具。如果确实需要加二次确认机制。记住Agent 再聪明也是按你的工具定义行事你能控制的是给它多大的能力边界。6.4 性能与并发的小技巧stdio 是单进程通信如果工具执行很慢比如跑测试会阻塞后续请求。解决办法是把耗时操作放到独立线程或进程里用异步返回。另外日志搜索这种操作如果文件很大可以考虑先建索引而不是每次全量扫描。7. 关于 MCP 的一些个人体会我从去年开始陆续把团队内部的几个工具都封装成了 MCP Server最大的感受是它把给 AI 接工具这件事从一次性劳动变成了可复用资产。以前每换一个 AI 客户端就要重写一遍适配现在写一次到处能用。这个价值在工具数量多起来之后尤其明显。另一个体会是MCP 的门槛其实比想象中低。核心就是 JSON-RPC 加 stdio一个下午就能写出能用的 Server。真正花时间的是设计好工具边界——哪些能力该暴露、参数怎么设计、描述怎么写才能让模型准确调用。这部分没有标准答案只能在实际使用中不断调整。如果你也想动手我的建议是从一个最小场景开始比如就做一个读取指定文件的工具跑通整个链路再逐步加功能。别一上来就想着做全能工具集那样很容易在调试阶段就放弃。跑通第一个工具的那一刻你会对 Agent 和 MCP 的关系有完全不一样的理解。
返回列表