Skip to content
掌握度

CodeAgent CLI 项目深挖 · 单期模块。总览见 需求分析 与 整体设计方案。

MCP(Model Context Protocol)让 CodeAgent CLI 连接外部的 MCP server,把 server 暴露的工具动态注册成 Agent 可调用的工具(命名 mcp__{server}__{tool}),从而把 Agent 的能力边界从「16 个内置工具」扩展到「任意第三方能力」。核心是一个分层架构:传输层 → JSON-RPC → MCP 协议 → 服务器管理 → 工具注册。

MCP 动态工具 ​

McpServerManager.start:读配置 → Stdio/StreamableHTTP transport → McpClient.initialize 握手 → listTools → 以 mcp__{server}__{tool} 命名注册进 Registry(McpSchemaSanitizer 清洗 $ref/anyOf 保证 Schema 合法);resources 能力自动注册两个虚拟工具;NotificationRouter 处理 tools/list_changed 动态重挂;启动用 bounded wait 超时后后台继续起——MCP Server 挂了不能拖死 CLI 启动。

MCP 集成实现 ​

从下往上五层。

第一层:传输(Transport)。 抽象接口 McpTransport,两种实现:StdioTransport(本地进程,ProcessBuilder 启动 npx/uvx 命令,通过 stdin/stdout 收发 JSON 消息,)和 StreamableHttpTransport(远程 HTTP)。第十期核心是 stdio,第十一期加了 HTTP。

第二层:JSON-RPC。 JsonRpcClient 负责请求/响应配对:每次 request 生成自增 id、放进 pending 的 ConcurrentHashMap<Long, CompletableFuture>,收到响应按 id 找到对应 Future 完成,带超时(默认 60 秒,ScheduledExecutorService 兜底)。没有 id 的消息是通知(notification),分发给 listener。

第三层:MCP 协议(McpClient)。 封装 MCP 语义:initialize 握手拿 server capabilities、listTools 拉工具列表、callToolOutput 调工具、listResources / readResource 读资源。

第四层:服务器管理(McpServerManager)。 启动流程:configLoader.prepare 展开配置 → createTransport 建传输 → client.initialize 握手 → buildToolList 拉工具 → replaceTools 注册进 ToolRegistry。

第五层:工具注册。 每个 MCP 工具被注册成名字 mcp__{server}__{tool} 的动态工具,调用时把 LLM 给的 JSON 参数透传给 server 的 tools/call。

【主动抛深度点 1:SchemaSanitizer —— 第三方的 schema 不可信】 这是最容易被忽视但很重要的设计。MCP server 是第三方的,它返回的工具 inputSchema 可能含 $schema / $ref / anyOf 这些 LLM 不友好、甚至会让下游解析出错的东西。所以 McpSchemaSanitizer 在注册前清洗 schema:剥掉 $schema / $id / $ref,把 anyOf / oneOf 拍平成一段描述文本,强制补 type 和 properties,描述超长截断到 1000 字符。本质是「把不可控的第三方输入,规整成可控的、LLM 能稳定理解的格式」。

【主动抛深度点 2:resources 虚拟工具 + @mention(第十一期高级)】 MCP 不只有工具,还有 resources(数据资源,如文件、数据库表)。因为 LLM 只能调工具,所以我把 resources 也包装成两个虚拟工具:mcp__{server}__list_resources 和 mcp__{server}__read_resource。同时支持用户在输入里用 @server:uri 直接引用 resource(AtMentionParser 用正则解析,)。这样 resource 既能被模型主动发现,也能被用户显式点名。

MCP 为什么用 JSON-RPC,而不是自定义协议? ​

因为 MCP 协议本身就定义在 JSON-RPC 2.0 之上,这是协议的规范要求,不是我们自选。好处是成熟、简单——请求/响应用 id 配对、通知不带 id,我只需要一个 ConcurrentHashMap 存 pending 请求就能实现异步配对。不用自己造一套消息格式。

stdio 和 HTTP 两种传输有什么区别? ​

stdio 是本地进程:ProcessBuilder 起一个 npx / uvx 命令,JSON 消息走 stdin/stdout,适合本地工具(比如本地跑一个 MCP server)。HTTP 是远程服务:走 streamable HTTP,适合连远程 MCP server(比如 StepSearch)。两种都实现同一个 McpTransport 接口,上层 JsonRpcClient 和 McpClient 完全无感——这是「传输可插拔」的关键。

工具怎么命名和注册?调用时怎么分发? ​

命名是 mcp__{server}__{tool} 三段式(用 __ 分隔),保证不同 server 的同名工具不冲突。注册时 McpServerManager 把 McpToolDescriptor 列表交给 ToolRegistry 的 replaceMcpToolOutputsForServer,每个工具的 executor 就是 client.callToolOutput(...)。调用时 LLM 给出 mcp__xxx__yyy 这个名字,ToolRegistry 找到对应的 invoker,把参数 JSON 透传给 server 的 tools/call。

${VAR} 环境变量怎么展开?配置怎么合并? ​

配置文件有两级合并:先读用户级 ~/.codeagent/mcp.json,再读项目级 .codeagent/mcp.json,后读的覆盖先读的(同名 server 项目级优先)。配置里的 ${VAR} 用正则匹配展开,取值顺序是系统环境变量 → 系统属性 → 项目 .env → 用户 ~/.env,展开在启动单个 server 时做——单个 server 缺环境变量只让它报 ERROR,不阻塞其他 server。

MCP 启动会阻塞首屏吗? ​

不会。启动用 CompletableFuture.allOf 带 maxWait 超时(默认 8 秒),超时就把还没起来的 server 标成 STARTING、后台 daemon 线程继续启动,CLI 先出首屏。用户可以用 /mcp 看最新状态、/mcp logs <name> 看日志。这是为了不让一个慢的 stdio/http server(比如 npx 冷启动)绑架整个 CLI 的启动体验。

八股延伸|MCP 和 tool 之间有什么联系和区别 ​

MCP 和 tool 之间有什么联系和区别

“区别看归属。tool 是应用内的函数,我自己写、自己注册、跟着代码走,别的应用用不了。”

“MCP 把工具从应用里拆出来,变成独立的服务进程,任何支持 MCP 的客户端都能连上来用。解决的是 M 个应用对接 N 个工具的组合爆炸问题。”

“联系是殊途同归。MCP 工具连上来之后,会被包装成和内置工具一样的形态,注册进同一张工具注册表,最终以函数调用(Function Calling)的格式一起喂给模型。”

“模型不知道也不需要知道,一个工具到底是本地函数还是远端服务。”

“实现上我做了三件事:”

  • 传输支持 stdio 和 Streamable HTTP 两种
  • 远端工具名加服务名前缀做命名空间隔离,防止和内置工具撞名
  • 权限上参考远端声明的只读提示,非只读的 MCP 工具一律人工确认后才执行

“配置也是分层合并的。用户目录一份全局配置,项目目录一份局部配置,同名服务后者覆盖前者,路径里支持环境变量展开。”

“某个 server 连不上时单独隔离并记下错误,不影响其他工具正常注册。”

“还有个容易被忽略的点。MCP 除了工具还有资源和提示词模板,我把它们也映射成了虚拟工具,列资源、读资源和调用普通工具走的是同一条路径,模型侧不用学新动作。”

八股延伸|MCP 协议解决了什么问题? ​

MCP 协议解决了什么问题?

“一句话概括:MCP 解决的是 Agent 工具接入的标准化问题。”

在没有 MCP 之前,每接一个外部工具,就得在 Agent 代码里硬编码一套调用逻辑,HTTP 调一套、CLI 调一套、SDK 调又一套,参数格式、错误处理全不统一。MCP 把工具接入抽象成了一个统一的协议层。

怎么解决的? ​

“三步。第一步是传输层抽象。MCP 定义了两种标准传输方式:Stdio(标准输入输出,适合本地进程)和 Streamable HTTP(适合远程服务)。Agent 端不用关心底层是进程通信还是网络请求,只要实现同一套传输接口就行。”

“第二步是通信协议。基于 JSON-RPC 2.0,只有三种消息:Request(带 id,需要响应)、Response(带 id,匹配请求)、Notification(无 id,单向通知)。整个生命周期就是 initialize → tools/list → tools/call → close。”

“第三步是工具发现。MCP Server 启动后,Agent 调 tools/list 就能拿到这个 Server 暴露的所有工具的名称、描述和参数 JSON Schema。”

Agent 把这些工具自动注册到自己的工具注册表里,LLM 就能像调内置工具一样调 MCP 工具。

整个过程是动态的,MCP Server 还能发 notifications/tools/list_changed 通知,Agent 会自动重新拉取工具列表并更新注册。

源码锚点(读码定位用,面试不主动讲) ​

  • JsonRpcClient.java:91
  • McpClient.java:41
  • McpServerManager.java:401-428
  • McpSchemaSanitizer.java:43
  • McpResourceTool.java:22
  • McpConfigLoader.java:140
  • McpServerManager.java:101-107
  • StdioTransport.java:52
  • AtMentionParser.java:9

本站仅供个人学习使用,请勿外传