MCP协议实战:从零构建一个能读日志和代码的Bug检查Agent服务

发布时间:2026/10/7 2:05:08

MCP协议实战:从零构建一个能读日志和代码的Bug检查Agent服务 1. 从一个真实场景说起为什么需要让 Agent 替我看 Bug做后端开发的朋友大概率都经历过这种时刻凌晨两点线上告警响了日志里一堆堆栈信息你揉着眼睛翻代码心里想的是要是能有个东西帮我把这些报错先过一遍就好了。这个念头其实指向了一个很实际的需求——让 AI Agent 具备读取和分析代码仓库、日志文件、接口返回的能力而不是每次都要人手动复制粘贴。过去我们让大模型帮忙看 Bug流程基本是这样的打开聊天窗口把报错信息复制进去再把相关代码片段复制进去然后等它回复。如果它说我需要看看这个函数的上下文你又得回去找代码再贴一遍。整个过程下来人其实没省多少事反而在搬运信息上花了不少时间。MCPModel Context Protocol要解决的就是这个搬运问题。它本质上是一套标准化的协议让 AI 模型能够以统一的方式去调用外部工具、读取外部资源。你可以把它理解成给 AI 装了一个USB 接口——不管对面插的是文件系统、数据库还是某个 API 服务只要符合这个接口规范AI 就能直接访问不需要你每次手动喂数据。这篇文章我会用一个让 Agent 替你看 Bug的简单示例把 MCP 服务的核心机制讲清楚。重点不是教你搭一个多复杂的系统而是让你理解 MCP 到底怎么工作、stdio 传输是怎么回事、JSON 消息长什么样、以及为什么这套东西能让 Agent 真正动起来。适合有一定编程基础、想搞清楚 MCP 是什么但又被官方文档绕晕的开发者。2. MCP 到底解决了什么问题从人肉搬运到协议调用2.1 没有 MCP 之前Agent 是怎么干活的在 MCP 出现之前让大模型调用外部工具主要有几种做法。一种是在 Prompt 里硬编码工具描述告诉模型你有这些函数可以调用然后模型输出特定格式的文本你在代码里解析这个文本再去执行。这种做法能用但每换一个模型、每换一个工具格式就得重新对齐维护成本很高。另一种是各家平台自己定义的插件体系比如某些平台有自己的 Function Calling 规范。问题是这些规范互不兼容你为 A 平台写的工具换到 B 平台就得重写。对于开发者来说这意味着大量的重复劳动。还有一个更根本的问题工具和模型之间的通信是一次性的。模型调用一个工具拿到结果然后继续生成。但如果工具需要多轮交互呢比如读取一个目录发现里面有子目录再进去读这种链式操作在早期方案里很难优雅地实现。2.2 MCP 的核心思路把工具变成服务MCP 的做法是把工具抽象成服务用一套统一的协议来描述和调用。这套协议基于 JSON-RPC 2.0消息格式是 JSON传输方式可以是 stdio标准输入输出也可以是 HTTP。模型这边通过 MCP 客户端连接服务服务这边暴露工具Tools资源Resources提示Prompts三类能力。这里的关键在于标准化。一旦协议统一了工具开发者只需要按照 MCP 规范实现一次就能被所有支持 MCP 的客户端使用。模型这边也不需要关心工具的具体实现只需要知道有这么个工具参数是什么返回什么。用一个类比来说以前的工具调用像是每家店自己印优惠券格式五花八门MCP 相当于推出了一个统一的优惠券标准所有店都按这个标准来消费者拿着一张券就能到处用。2.3 为什么 stdio 是最适合入门的传输方式MCP 支持多种传输方式但对于本地开发和入门理解来说stdio 是最直观的。stdio 就是标准输入输出——服务进程启动后通过 stdin 接收消息通过 stdout 返回消息。没有网络层没有端口占用没有认证握手就是两个进程之间通过管道对话。这种方式的优点是简单、可控、容易调试。你可以直接在终端里手动输入 JSON 消息看服务返回什么。对于理解 MCP 的消息格式和交互流程来说没有比这更直接的方式了。当然 stdio 也有局限它只适合本地进程间通信不适合远程服务。但对于让 Agent 看本地代码仓库里的 Bug这个场景来说stdio 完全够用而且更安全——服务只在本机运行不暴露任何网络端口。3. 拆解一个最小 MCP 服务从消息格式到工具注册3.1 JSON-RPC 2.0 的消息长什么样MCP 的通信基于 JSON-RPC 2.0这个协议规定了请求、响应、通知三种消息格式。请求消息包含jsonrpc、id、method、params四个字段响应消息包含jsonrpc、id、result或error。一个典型的初始化请求是这样的{ jsonrpc: 2.0, id: 1, method: initialize, params: { protocolVersion: 2024-11-05, capabilities: {}, clientInfo: { name: my-client, version: 1.0.0 } } }服务收到后会返回自己的能力声明{ jsonrpc: 2.0, id: 1, result: { protocolVersion: 2024-11-05, capabilities: { tools: {} }, serverInfo: { name: bug-inspector, version: 1.0.0 } } }这个握手过程很重要它让客户端知道服务支持哪些能力。如果服务声明了tools客户端就知道可以调用tools/list来获取工具列表。3.2 工具注册告诉 Agent 你能干什么工具注册是 MCP 服务的核心。每个工具需要定义名称、描述、输入参数的 JSON Schema。以读取文件内容这个工具为例{ name: read_file, description: 读取指定路径的文件内容用于查看代码或日志, inputSchema: { type: object, properties: { path: { type: string, description: 文件的绝对路径或相对路径 } }, required: [path] } }这里的description非常关键。模型就是靠这段描述来判断什么时候该用这个工具。如果描述写得含糊模型可能在该调用的时候不调用或者在不该调用的时候乱调用。我的经验是描述里最好包含什么时候用和用来干什么而不是只写读取文件。inputSchema用的是 JSON Schema 标准这意味着参数的类型、是否必填、取值范围都可以精确描述。模型会根据这个 Schema 来生成调用参数Schema 写得越清楚模型生成的参数就越准确。3.3 工具调用的完整链路当 Agent 决定调用某个工具时客户端会发送tools/call请求{ jsonrpc: 2.0, id: 2, method: tools/call, params: { name: read_file, arguments: { path: /var/log/app/error.log } } }服务执行读取操作后返回结果{ jsonrpc: 2.0, id: 2, result: { content: [ { type: text, text: 2024-01-15 02:33:17 ERROR ... } ] } }注意content是一个数组这意味着一个工具调用可以返回多种类型的内容比如文本、图片、资源引用等。对于看 Bug 这个场景返回文本就够了。整个链路走下来Agent 的工作流程是先通过tools/list拿到可用工具然后根据用户的问题决定调用哪个工具发送tools/call拿到结果后继续推理。如果一次调用不够它可以连续调用多次这就是所谓的多轮工具调用。4. 动手实现一个能读日志和代码的 Bug 检查服务4.1 技术选型为什么用 Python 而不是 Node实现 MCP 服务可以用任何语言官方提供了 Python 和 TypeScript 的 SDK。我选 Python 的原因很简单处理文本、正则匹配、日志解析这些事Python 的生态更顺手。而且对于看 Bug这个场景经常需要对日志做模式提取Python 的re模块和字符串处理能力用起来更自然。如果你更熟悉 JavaScript用 TypeScript SDK 也完全没问题协议层面是一样的。选型的核心原则是用你最熟悉的语言因为 MCP 的复杂度在协议理解上不在语言本身。4.2 服务骨架从 stdio 读取消息一个最小的 MCP 服务骨架大概是这样import sys import json def read_message(): line sys.stdin.readline() if not line: return None return json.loads(line) def write_message(msg): sys.stdout.write(json.dumps(msg) \n) sys.stdout.flush() def main(): while True: msg read_message() if msg is None: break handle_message(msg) def handle_message(msg): method msg.get(method) msg_id msg.get(id) if method initialize: write_message({ jsonrpc: 2.0, id: msg_id, result: { protocolVersion: 2024-11-05, capabilities: {tools: {}}, serverInfo: {name: bug-inspector, version: 1.0.0} } }) elif method tools/list: write_message({ jsonrpc: 2.0, id: msg_id, result: {tools: get_tools()} }) elif method tools/call: result call_tool(msg[params]) write_message({ jsonrpc: 2.0, id: msg_id, result: result }) if __name__ __main__: main()这段代码有几个细节值得注意。第一每条消息占一行用换行符分隔这是 stdio 传输的约定。第二写完消息后必须flush否则消息可能留在缓冲区里客户端收不到。第三initialize必须返回正确的protocolVersion版本不匹配客户端可能会拒绝连接。4.3 实现 read_file 和 search_log 两个工具有了骨架接下来实现具体工具。read_file负责读取文件内容search_log负责在日志里搜索关键词import os import re def get_tools(): return [ { name: read_file, description: 读取指定路径的文件内容。当需要查看代码文件或日志文件的具体内容时使用。, inputSchema: { type: object, properties: { path: {type: string, description: 文件路径} }, required: [path] } }, { name: search_log, description: 在日志文件中搜索包含指定关键词的行返回匹配行及其上下文。当需要定位错误信息时使用。, inputSchema: { type: object, properties: { path: {type: string, description: 日志文件路径}, keyword: {type: string, description: 搜索关键词如 ERROR、Exception}, context_lines: {type: integer, description: 匹配行的上下文行数默认 3} }, required: [path, keyword] } } ] def call_tool(params): name params[name] args params[arguments] if name read_file: return read_file(args[path]) elif name search_log: return search_log( args[path], args[keyword], args.get(context_lines, 3) ) else: return {content: [{type: text, text: f未知工具: {name}}], isError: True} def read_file(path): try: with open(path, r, encodingutf-8) as f: content f.read() return {content: [{type: text, text: content}]} except Exception as e: return {content: [{type: text, text: f读取失败: {str(e)}}], isError: True} def search_log(path, keyword, context_lines3): try: with open(path, r, encodingutf-8) as f: lines f.readlines() matches [] for i, line in enumerate(lines): if keyword in line: start max(0, i - context_lines) end min(len(lines), i context_lines 1) matches.append(.join(lines[start:end])) if not matches: return {content: [{type: text, text: f未找到包含 {keyword} 的行}]} return {content: [{type: text, text: \n---\n.join(matches)}]} except Exception as e: return {content: [{type: text, text: f搜索失败: {str(e)}}], isError: True}search_log这个工具的设计有个小心思它返回的是匹配行加上下文而不是整个文件。这样做的好处是减少传给模型的数据量同时保留足够的上下文让模型理解错误发生的场景。如果直接把整个日志文件丢给模型token 消耗会很大而且模型容易被无关信息干扰。4.4 在客户端配置里挂载这个服务服务写好了接下来要在 MCP 客户端里配置。以常见的配置文件格式为例{ mcpServers: { bug-inspector: { command: python, args: [/path/to/bug_inspector.py] } } }这段配置告诉客户端启动一个叫bug-inspector的服务用python命令执行指定脚本。客户端会自动管理这个进程的生命周期通过 stdio 和它通信。配置完成后Agent 就能看到read_file和search_log这两个工具了。当你问它帮我看看 /var/log/app/error.log 里有什么错误时它会自动调用search_log拿到结果后再分析。5. 实测中踩到的坑和排查思路5.1 消息没有换行导致客户端卡死我第一次跑通这个服务的时候遇到一个很典型的问题客户端启动后一直没反应像是卡住了。排查了半天才发现是write_message里忘了加换行符。stdio 传输的约定是一行一条消息如果服务返回的 JSON 后面没有\n客户端会一直等这一行结束自然就卡住了。这个问题的隐蔽之处在于服务本身没报错日志也正常就是客户端收不到消息。修复方法很简单在json.dumps后面加 \n。但更重要的是养成习惯所有通过 stdio 输出的消息都要确保以换行结尾。如果你用官方 SDKSDK 会帮你处理这个细节但自己手写的时候一定要记住。5.2 日志输出污染了 stdout第二个坑更隐蔽。我在服务里加了一些print语句用于调试结果发现客户端开始报无法解析消息的错误。原因是print默认输出到 stdout而 stdout 是 MCP 的消息通道调试信息混进去就把协议消息搞乱了。这个问题的解决方案是所有调试信息都输出到 stderr而不是 stdout。stderr 不会被 MCP 客户端解析可以安全地用来打日志。在 Python 里可以用print(..., filesys.stderr)或者用logging模块配置输出到 stderr。记住一条铁律stdout 只用来发 MCP 消息其他所有输出都走 stderr。5.3 工具描述写得太模糊导致模型不调用工具能跑通之后我发现模型有时候该调用search_log的时候不调用而是直接凭猜测回答。检查后发现是工具描述的问题。我最初写的是搜索日志模型不太确定什么时候该用。改成在日志文件中搜索包含指定关键词的行返回匹配行及其上下文。当需要定位错误信息时使用之后调用准确率明显提升。这说明工具描述不只是给人看的更是给模型看的。描述里要明确这个工具做什么和什么时候用它。5.4 大文件读取导致上下文超限还有一个实际使用中遇到的问题如果read_file读取一个几万行的日志文件返回的内容会非常长可能超出模型的上下文限制。这时候模型要么报错要么只能看到文件开头的一部分。解决思路有两个。一是限制返回内容的大小比如只返回前 N 行并在描述里说明。二是引导模型先用search_log定位再用read_file读取特定范围。我在实际项目里采用的是第二种因为看 Bug 的场景通常不需要读整个文件而是需要定位到出问题的那几行。6. 从示例到实用MCP 服务的扩展方向6.1 增加代码搜索能力基础的read_file和search_log能应付简单场景但实际看 Bug 时经常需要在代码库里搜索某个函数在哪里定义。这时候可以加一个search_code工具用正则或简单的文本匹配在指定目录下搜索。实现上可以用os.walk遍历目录对每个文件做匹配。需要注意的是要过滤掉二进制文件和无关目录如.git、node_modules否则搜索会很慢。返回结果时最好带上文件名和行号方便模型定位。6.2 接入 Git 历史查看变更很多 Bug 是最近某次提交引入的。如果能提供一个git_log或git_diff工具让 Agent 查看最近的代码变更定位问题的效率会高很多。实现上可以调用git命令解析输出后返回。这个工具的价值在于它让 Agent 不仅能看现在是什么样还能看之前是什么样。对于排查回归问题特别有用。6.3 用 Resources 暴露项目结构MCP 除了 Tools还有 Resources 的概念。Resources 适合暴露那些只读的、结构化的信息比如项目目录树、配置文件内容。和 Tools 的区别在于Resources 通常是被动读取的而 Tools 是主动调用的。把项目结构做成 Resource 暴露出去Agent 就能在需要的时候查看目录结构而不需要每次都调用工具去列目录。这种设计更符合资源的语义。6.4 多服务组合的注意事项当你有多个 MCP 服务时要注意工具名称的冲突问题。如果两个服务都定义了read_file客户端可能会混淆。解决办法是给工具名加前缀比如fs_read_file、log_read_file或者在服务层面做好命名规范。另外多个服务同时运行时资源占用也需要关注。每个服务都是一个独立进程如果服务太多内存和 CPU 会有压力。实际使用中建议按功能拆分不要把所有工具都塞进一个服务也不要拆得太碎。7. 我对 MCP 这套机制的实际体会用下来最深的感受是MCP 的价值不在于技术有多复杂而在于它把工具调用这件事标准化了。以前每接一个新工具都要写适配代码现在只要服务符合 MCP 规范客户端就能直接用。这种标准化带来的效率提升在工具数量多起来之后会非常明显。另一个体会是工具描述的质量直接决定了 Agent 的表现。同样一套工具描述写得好和写得差模型的使用效果可能差出一大截。这有点像写 API 文档——文档写得好调用者用起来就顺写得含糊调用者就容易用错。给模型写工具描述本质上是在给一个很聪明但需要明确指令的协作者写说明书。最后分享一个小技巧调试 MCP 服务时可以先用echo命令手动发消息测试。比如echo {jsonrpc:2.0,id:1,method:tools/list} | python bug_inspector.py这样能快速验证服务的基本响应是否正常比每次都启动完整客户端要快得多。这个方法在我排查 stdio 相关问题时帮了不少忙。
延伸阅读

更多相关文章

2026/10/7 2:05:08

基于孪生网络与YOLOv3-tiny的点选验证码识别实战

简介:本资源是一套基于孪生神经网络实现点选识别验证码的完整项目源码,面向计算机、人工智能、通信工程等专业的在校学生、教师及企业员工,适合作为毕业设计、课程设计、作业或项目初期立项演示,也适合具备一定基础的小白进阶学习…

2026/10/7 2:00:07

银河麒麟V10内存不释放?MemAvailable与定时清理实战解析

简介:面向银河麒麟V10服务器运维人员的内存泄漏排查与定时清理方案,解决系统长时间运行后可用内存逐渐减少、性能下降甚至宕机的隐患。压缩包共3个文件,含2个shell脚本与1个txt配置说明,脚本用于定时监控并释放内存,tx…

2026/10/7 2:00:07

WPF嵌入Chrome内核浏览器:CefSharp选型与实战避坑指南

简介:基于Chrome内核的WPF浏览器开发,是一套用于在Windows桌面应用中嵌入现代浏览器的源码工程。它面向WPF开发者和希望为产品增加网页能力的技术人员,解决了传统WebBrowser控件兼容性弱、功能受限的问题,借助Chromium获得快速、稳…

2026/10/7 3:05:10

TRex服务能力解析:从单机流量工具到可集成的测试服务

你在做自动化测试平台或者大规模网络验收的时候,很快就会意识到一个问题:再好的流量发生器,如果只能坐在机房里敲命令行,那它就是一个高级玩具。TRex之所以能在高性能流量工具里站稳脚跟,不只是因为它基于DPDK能打出线…

2026/10/7 3:05:10

vi与Docker实战指南:从容器基础命令到高效运维

做 Linux 运维和开发这些年,我见过太多人被两个东西劝退:一个是 vi,进去之后不知道怎么退出;另一个是 Docker,装完不知道容器和镜像到底啥关系。vi 是 Linux 环境里最底层的编辑工具,Docker 是现在部署应用…

2026/10/7 3:05:10

福建DEM原始高程TIF数据:从选型、拼接到三维建模全攻略

简介:福建省数字高程模型(DEM)原始高程数据以TIFF格式存储,面向ArcGIS等地理信息软件的使用者,适合开展地形分析、水文模拟、灾害评估与城乡空间规划。压缩包整体约三百六十二兆字节,共三十个文件&#xff…

2026/10/7 3:05:10

C语言递归实战:从栈帧原理到高频题型拆解

带过C语言的人都体验过那种状态:盯着屏幕上十几行递归代码,明明每一行都认识,可函数一调用自己,脑内就立刻乱成一锅粥。在很多技术社群里,有个高频提问来回出现——“递归到底怎么想到这么写的?”我当年也在…

2026/10/7 3:05:10

Unity新输入系统实战指南:Action抽象与跨平台配置

说实话,Unity 从 2019 年开始把新输入系统(Input System)包塞进 Package Manager 的时候,我是持观望态度的。那会儿项目组里人多嘴杂,老的Input.GetAxis用得顺手,改接口、改配置、改设备兼容逻辑&#xff0…

2026/10/7 3:00:10

2026降重会不会影响质量?7款工具六维度实测打分

论文降重到底会不会把内容改坏?这个焦虑几乎每个毕业生都经历过。重复率是降下来了,可导师一句"这段读着不像人写的"又让人心里打鼓。为回答这个问题,笔者耗时三周,围绕生成质量、语义保留、上下文连贯、降重效率、功能…

2026/10/5 6:32:56

Jev+Agent接管浏览器:browser-use实战与jev-ultrafast性能优化

1. 从“Jev”说起:为什么我要把Agent接进浏览器“Jev”这个词最近在圈子里出现的频率越来越高,很多人第一次听到会以为是某个新模型的名字,其实它更像是一种思路——把Jev模型的能力当作底座,通过Agent的方式去接管浏览器&#xf…

2026/10/6 4:01:51

多智能体集群实战:DeepAgents编排、MCP与A2A协议及Skills体系

1. 从"单兵作战"到"集群协同":多智能体编排到底在解决什么问题如果你最近在折腾 Agent 相关的东西,大概率会有一种感觉:单个 Agent 能做的事情,其实很快就摸到天花板了。你给它一个提示词,挂几个工…

2026/10/6 17:46:51

无源低通滤波器设计实战:从RC到LC,手把手教你避开那些坑

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/7 1:05:03

ESP32免重刷固件:浏览器直接修改NVS键值实现WiFi配置更新

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/7 1:05:03

SAP HANA查询结果导出CSV:避开乱码、性能与权限的实用指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/7 1:05:03

数字后端Placement阶段Density与Congestion控制实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑