一个 Agent 系统的长期维护成本不在模型不在向量库——在工具怎么注册、能力怎么对外暴露这些连接层代码不是最多的却是最容易腐烂的。本文以前 11 篇中实际运行的 Skill 体系为起点讨论如何用 MCPModel Context Protocol标准化工具暴露并解决由此引发的深层架构矛盾。先看现状工具注册的胶水打开核心文件胶水层一览无余胶水工具注册SkillLoaderToolService新增一个工具要走三条私有路径写SKILL.md私有格式定义 frontmatter→ 在工具注册表手工加一行映射 → 在 Agent 模块注册 LangChain tool。dispatch 本身很干净——字典 O(1) 查表——但注册表仍然是手工维护的新工具不加映射就不可达。这里的关键词不是能不能用而是能维护多久。胶水代码没有技术壁垒只有认知壁垒——时间长了大家都不记得了。MCP让工具暴露从私有格式变成标准协议MCP 的核心模型极其简单——客户端与服务端之间只走 JSON-RPC客户端AI 应用 / MCP Inspector / n8n ↓ JSON-RPC over stdio / Streamable HTTP 服务端MCP Server ├── tools/list → 我能做什么自动从 SkillRegistry 生成 ├── tools/call → 帮我做这件事映射到 ToolService.dispatch ├── resources/read → 我可以看什么资料 └── prompts/get → 调用 prompt 模板Shop-Agent 在 MCP 生态中有两个方向作为 Client消费外部业务系统订单、物流的 MCP 工具——外部系统用什么语言写的不用关心作为 Server将自己的 Skill 体系对外暴露——Claude Desktop、n8n 等通过标准协议直接调用query-order、check-shipping。核心原则MCP 对外不对内——ReActAgent ↔ ToolService 内部走直接函数调用同进程函数调用不需要 JSON-RPC 序列化开销。MCP Server 核心架构自动注册不手写映射表。初始化时遍历SkillRegistry把每个 skill 自动注册为 MCP tool——新增工具只需要写SKILL.md并实现对应的工具方法MCP 层自动感知。工具名、描述、参数全部来自 SkillRegistry与tools/list同源。类型注解驱动 Schema不手写 JSON。通过动态生成带类型注解的 Python 函数如async def query_order(order_id: str|None None, phone: str|None None) - strFastMCP 从类型注解自动推断inputSchema——tools/list返回的参数描述始终与ToolService的定义保持同步。挂载到主应用不另开端口。MCP Server 通过app.mount(/mcp, ...)挂到 FastAPI与业务 API 共用 8000 端口。传输层使用 Streamable HTTPSSE 已在 MCP SDK 1.28 中被弃用。⚠一个坑FastAPI 的app.mount()不会自动执行子应用的 lifespan需要在主 lifespan 中手动初始化 MCP 的SessionManager。另外FastAPI 0.115.x 与 Starlette 1.3.x 的on_startup参数不兼容导致路由匹配失效——需升级到 FastAPI ≥ 0.130。已注册的 5 个工具从skills/目录自动加载工具名参数描述query-orderorder_id,phone查询订单状态/列表check-shippingtracking_number,order_id查询物流轨迹request-returnorder_id,reason申请退货退款check-balance(无参数)查询余额/积分coupon-inquirycoupon_type查询优惠券MCP vs HTTP“这和 HTTP API 有什么区别”——最常被问到的问题。维度HTTP/REST APIMCP发现机制需要开发者知道 OpenAPI 端点地址人驱动、主动拉取tools/list协议内置客户端连接即获取机器驱动Schema 自描述OpenAPI 与调用协议分离——schema 更新不保证调用端同步感知inputSchema与tools/call在同一协议通道内——声明即生效调用协议每家自定义URL、方法、错误格式各不相同统一tools/callJSON-RPC一种方式覆盖所有工具LLM 友好度需要开发者把 Swagger 翻译成 function calling 格式inputSchema本身就是 JSON Schema直接喂给 LLM适用场景前端调用、传统后端集成Agent-to-Tool标准化协议一句话HTTP 给人用MCP 给 Agent 用。两者不互斥——同一服务可同时提供 HTTP 端点和 MCP Server。鉴权与暴露策略MCP 协议尚无强制统一鉴权——stdio 靠 OS 进程隔离Streamable HTTP 在 Header 透传 Bearer Token与第九篇的认证体系一脉相承。实际关键是参数级鉴权同一个tools/call不同 token 查询范围不同ops-token 仅查物流状态不能看个人信息。不是所有工具都对外开放。以 Shop-Agent 为例query-order、check-shipping——只读、无副作用request-return、check-balance走 HITL 安全链路。2-3 个精心挑选的工具足以覆盖 90% 的集成场景。MCP Client消费外部服务上面讨论的是把门打开让别人进来——Shop-Agent 作为 Server 暴露工具。反过来还有通过标准门出去调别人——Shop-Agent 消费外部业务系统。现状是工具调用层用硬编码的 URL 映射表把 action 翻译成 HTTP 端点参数名也需要手动转换。新增一个外部工具就要维护两张表——又是胶水。MCP Client 的做法让外部业务系统也暴露 MCP ServerShop-Agent 连接后通过tools/list自动发现可用工具及其inputSchema不再需要手写映射表。调用统一走tools/call不管远端是 Java、Go 还是 Python协议一致。MCP_CLIENT_SERVERS配置一个 JSON 数组即可——列出各外部 Server 的名称、地址和可选鉴权头启动时自动连接并拉取工具清单。同一工具的调用优先级MCP Client 优先未命中再回退到 HTTP 映射表。MCP Server 不可用时业务照常运行只升级了集成方式。Server 和 Client 各司其职、互不冲突——共享tools/listtools/call同一套协议语汇。动态 Schema vs 静态正则MCP Server 包装完成后深层矛盾浮现。第二篇的LocalParamExtractor用正则提取参数——零 LLM 调用、毫秒级——但它把字段名硬编码在_EXTRACTORS字典里query-order对应提取order_id、check-shipping对应提取tracking_number。MCP 引入后tools/list返回的inputSchema是动态的——订单系统把order_id改成order_numberMCP Server 重启后 Agent 立刻看到新 schema但正则还在提取order_id。协议层动态自描述了参数提取层还是静态硬编码的。解法不是放弃正则而是让正则的结构层从 MCP schema 驱动——同时用一层 alias 消除同语义字段的重复。核心设计是三层解耦层数据来源变更时谁改结构层有哪些字段、叫什么名MCPinputSchemaMCP Server 自动同步Extractor 零改动语义层用什么正则匹配值本地_PATTERNS只在出现新语义类型时加一个正则alias 层MCP 字段名映射到哪种语义本地_FIELD_ALIASES字段改名时加一行 alias正则不重复alias 层的映射很简单order_id → order # 订单号语义匹配 GD\d|订单号[:]\s*(\S) order_num → order # 换了名指向同一语义复用同一个正则 tracking_number → tracking # 物流号语义 phone → phone # 手机号语义匹配 1[3-9]\d{9}当order_id改为order_number时Extractor 不改一行代码——它遍历mcp_schema[properties]的 key通过 alias 找到语义类型再拿对应的正则去匹配。正则不再硬编码有哪些字段它只回答一个问题“给定语义类型我该怎么从用户消息里找到它的值” 如果 alias 层没有匹配退化到 LLM 提取P2 兜底路径。当然一般来说确定好的接口版本参数不会出现不可预期的变化但是这样设计可以对参数变化保持动态适应性。核心逻辑不动——dispatch 逻辑原封不动映射到tools/call。MCP Server 包装仅仅在外面加了一层标准协议适配器。MCP 不能提升意图识别的准确率不能降低 RAG 延迟。它解决的问题是另一个层级的当订单系统改了一个字段名你的参数提取器是自动感知还是运行时静默失败好架构是让系统自己保持一致性。
MCP 协议实践 —— 让 Skill 体系从“私有胶水“走向“标准协议“
一个 Agent 系统的长期维护成本不在模型不在向量库——在工具怎么注册、能力怎么对外暴露这些连接层代码不是最多的却是最容易腐烂的。本文以前 11 篇中实际运行的 Skill 体系为起点讨论如何用 MCPModel Context Protocol标准化工具暴露并解决由此引发的深层架构矛盾。先看现状工具注册的胶水打开核心文件胶水层一览无余胶水工具注册SkillLoaderToolService新增一个工具要走三条私有路径写SKILL.md私有格式定义 frontmatter→ 在工具注册表手工加一行映射 → 在 Agent 模块注册 LangChain tool。dispatch 本身很干净——字典 O(1) 查表——但注册表仍然是手工维护的新工具不加映射就不可达。这里的关键词不是能不能用而是能维护多久。胶水代码没有技术壁垒只有认知壁垒——时间长了大家都不记得了。MCP让工具暴露从私有格式变成标准协议MCP 的核心模型极其简单——客户端与服务端之间只走 JSON-RPC客户端AI 应用 / MCP Inspector / n8n ↓ JSON-RPC over stdio / Streamable HTTP 服务端MCP Server ├── tools/list → 我能做什么自动从 SkillRegistry 生成 ├── tools/call → 帮我做这件事映射到 ToolService.dispatch ├── resources/read → 我可以看什么资料 └── prompts/get → 调用 prompt 模板Shop-Agent 在 MCP 生态中有两个方向作为 Client消费外部业务系统订单、物流的 MCP 工具——外部系统用什么语言写的不用关心作为 Server将自己的 Skill 体系对外暴露——Claude Desktop、n8n 等通过标准协议直接调用query-order、check-shipping。核心原则MCP 对外不对内——ReActAgent ↔ ToolService 内部走直接函数调用同进程函数调用不需要 JSON-RPC 序列化开销。MCP Server 核心架构自动注册不手写映射表。初始化时遍历SkillRegistry把每个 skill 自动注册为 MCP tool——新增工具只需要写SKILL.md并实现对应的工具方法MCP 层自动感知。工具名、描述、参数全部来自 SkillRegistry与tools/list同源。类型注解驱动 Schema不手写 JSON。通过动态生成带类型注解的 Python 函数如async def query_order(order_id: str|None None, phone: str|None None) - strFastMCP 从类型注解自动推断inputSchema——tools/list返回的参数描述始终与ToolService的定义保持同步。挂载到主应用不另开端口。MCP Server 通过app.mount(/mcp, ...)挂到 FastAPI与业务 API 共用 8000 端口。传输层使用 Streamable HTTPSSE 已在 MCP SDK 1.28 中被弃用。⚠一个坑FastAPI 的app.mount()不会自动执行子应用的 lifespan需要在主 lifespan 中手动初始化 MCP 的SessionManager。另外FastAPI 0.115.x 与 Starlette 1.3.x 的on_startup参数不兼容导致路由匹配失效——需升级到 FastAPI ≥ 0.130。已注册的 5 个工具从skills/目录自动加载工具名参数描述query-orderorder_id,phone查询订单状态/列表check-shippingtracking_number,order_id查询物流轨迹request-returnorder_id,reason申请退货退款check-balance(无参数)查询余额/积分coupon-inquirycoupon_type查询优惠券MCP vs HTTP“这和 HTTP API 有什么区别”——最常被问到的问题。维度HTTP/REST APIMCP发现机制需要开发者知道 OpenAPI 端点地址人驱动、主动拉取tools/list协议内置客户端连接即获取机器驱动Schema 自描述OpenAPI 与调用协议分离——schema 更新不保证调用端同步感知inputSchema与tools/call在同一协议通道内——声明即生效调用协议每家自定义URL、方法、错误格式各不相同统一tools/callJSON-RPC一种方式覆盖所有工具LLM 友好度需要开发者把 Swagger 翻译成 function calling 格式inputSchema本身就是 JSON Schema直接喂给 LLM适用场景前端调用、传统后端集成Agent-to-Tool标准化协议一句话HTTP 给人用MCP 给 Agent 用。两者不互斥——同一服务可同时提供 HTTP 端点和 MCP Server。鉴权与暴露策略MCP 协议尚无强制统一鉴权——stdio 靠 OS 进程隔离Streamable HTTP 在 Header 透传 Bearer Token与第九篇的认证体系一脉相承。实际关键是参数级鉴权同一个tools/call不同 token 查询范围不同ops-token 仅查物流状态不能看个人信息。不是所有工具都对外开放。以 Shop-Agent 为例query-order、check-shipping——只读、无副作用request-return、check-balance走 HITL 安全链路。2-3 个精心挑选的工具足以覆盖 90% 的集成场景。MCP Client消费外部服务上面讨论的是把门打开让别人进来——Shop-Agent 作为 Server 暴露工具。反过来还有通过标准门出去调别人——Shop-Agent 消费外部业务系统。现状是工具调用层用硬编码的 URL 映射表把 action 翻译成 HTTP 端点参数名也需要手动转换。新增一个外部工具就要维护两张表——又是胶水。MCP Client 的做法让外部业务系统也暴露 MCP ServerShop-Agent 连接后通过tools/list自动发现可用工具及其inputSchema不再需要手写映射表。调用统一走tools/call不管远端是 Java、Go 还是 Python协议一致。MCP_CLIENT_SERVERS配置一个 JSON 数组即可——列出各外部 Server 的名称、地址和可选鉴权头启动时自动连接并拉取工具清单。同一工具的调用优先级MCP Client 优先未命中再回退到 HTTP 映射表。MCP Server 不可用时业务照常运行只升级了集成方式。Server 和 Client 各司其职、互不冲突——共享tools/listtools/call同一套协议语汇。动态 Schema vs 静态正则MCP Server 包装完成后深层矛盾浮现。第二篇的LocalParamExtractor用正则提取参数——零 LLM 调用、毫秒级——但它把字段名硬编码在_EXTRACTORS字典里query-order对应提取order_id、check-shipping对应提取tracking_number。MCP 引入后tools/list返回的inputSchema是动态的——订单系统把order_id改成order_numberMCP Server 重启后 Agent 立刻看到新 schema但正则还在提取order_id。协议层动态自描述了参数提取层还是静态硬编码的。解法不是放弃正则而是让正则的结构层从 MCP schema 驱动——同时用一层 alias 消除同语义字段的重复。核心设计是三层解耦层数据来源变更时谁改结构层有哪些字段、叫什么名MCPinputSchemaMCP Server 自动同步Extractor 零改动语义层用什么正则匹配值本地_PATTERNS只在出现新语义类型时加一个正则alias 层MCP 字段名映射到哪种语义本地_FIELD_ALIASES字段改名时加一行 alias正则不重复alias 层的映射很简单order_id → order # 订单号语义匹配 GD\d|订单号[:]\s*(\S) order_num → order # 换了名指向同一语义复用同一个正则 tracking_number → tracking # 物流号语义 phone → phone # 手机号语义匹配 1[3-9]\d{9}当order_id改为order_number时Extractor 不改一行代码——它遍历mcp_schema[properties]的 key通过 alias 找到语义类型再拿对应的正则去匹配。正则不再硬编码有哪些字段它只回答一个问题“给定语义类型我该怎么从用户消息里找到它的值” 如果 alias 层没有匹配退化到 LLM 提取P2 兜底路径。当然一般来说确定好的接口版本参数不会出现不可预期的变化但是这样设计可以对参数变化保持动态适应性。核心逻辑不动——dispatch 逻辑原封不动映射到tools/call。MCP Server 包装仅仅在外面加了一层标准协议适配器。MCP 不能提升意图识别的准确率不能降低 RAG 延迟。它解决的问题是另一个层级的当订单系统改了一个字段名你的参数提取器是自动感知还是运行时静默失败好架构是让系统自己保持一致性。