一个 Agent 系统的长期维护成本不在模型,不在向量库——在工具怎么注册、能力怎么对外暴露,这些"连接层"代码不是最多的,却是最容易腐烂的。

本文以前 11 篇中实际运行的 Skill 体系为起点,讨论如何用 MCP(Model Context Protocol)标准化工具暴露,并解决由此引发的深层架构矛盾。


先看现状:工具注册的胶水

打开核心文件,胶水层一览无余:

胶水:工具注册(SkillLoader + ToolService

新增一个工具要走三条私有路径:写 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-ordercheck-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) -> str),FastMCP 从类型注解自动推断 inputSchema——tools/list 返回的参数描述始终与 ToolService 的定义保持同步。

挂载到主应用,不另开端口。 MCP Server 通过 app.mount("/mcp", ...) 挂到 FastAPI,与业务 API 共用 8000 端口。传输层使用 Streamable HTTP(SSE 已在 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-order order_id, phone 查询订单状态/列表
check-shipping tracking_number, order_id 查询物流轨迹
request-return order_id, reason 申请退货退款
check-balance (无参数) 查询余额/积分
coupon-inquiry coupon_type 查询优惠券
MCP vs HTTP

“这和 HTTP API 有什么区别?”——最常被问到的问题。

维度 HTTP/REST API MCP
发现机制 需要开发者知道 OpenAPI 端点地址,人驱动、主动拉取 tools/list 协议内置,客户端连接即获取,机器驱动
Schema 自描述 OpenAPI 与调用协议分离——schema 更新不保证调用端同步感知 inputSchematools/call 在同一协议通道内——声明即生效
调用协议 每家自定义(URL、方法、错误格式各不相同) 统一 tools/call(JSON-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-ordercheck-shipping ——只读、无副作用;request-returncheck-balance 走 HITL 安全链路。2-3 个精心挑选的工具足以覆盖 90% 的集成场景。

MCP Client:消费外部服务

上面讨论的是"把门打开让别人进来"——Shop-Agent 作为 Server 暴露工具。反过来还有"通过标准门出去调别人"——Shop-Agent 消费外部业务系统。

现状是工具调用层用硬编码的 URL 映射表把 action 翻译成 HTTP 端点,参数名也需要手动转换。新增一个外部工具就要维护两张表——又是胶水。

MCP Client 的做法:让外部业务系统也暴露 MCP Server,Shop-Agent 连接后通过 tools/list 自动发现可用工具及其 inputSchema,不再需要手写映射表。调用统一走 tools/call,不管远端是 Java、Go 还是 Python,协议一致。

MCP_CLIENT_SERVERS 配置一个 JSON 数组即可——列出各外部 Server 的名称、地址和可选鉴权头,启动时自动连接并拉取工具清单。

同一工具的调用优先级:MCP Client 优先,未命中再回退到 HTTP 映射表。MCP Server 不可用时业务照常运行,只升级了集成方式。

Server 和 Client 各司其职、互不冲突——共享 tools/list + tools/call 同一套协议语汇。


动态 Schema vs 静态正则

MCP Server 包装完成后,深层矛盾浮现。第二篇的 LocalParamExtractor 用正则提取参数——零 LLM 调用、毫秒级——但它把字段名硬编码在 _EXTRACTORS 字典里:"query-order" 对应提取 order_id"check-shipping" 对应提取 tracking_number。MCP 引入后,tools/list 返回的 inputSchema 是动态的——订单系统把 order_id 改成 order_number,MCP Server 重启后 Agent 立刻看到新 schema,但正则还在提取 order_id协议层动态自描述了,参数提取层还是静态硬编码的。

解法不是放弃正则,而是让正则的结构层从 MCP schema 驱动——同时用一层 alias 消除同语义字段的重复。核心设计是三层解耦:

数据来源 变更时谁改
结构层(有哪些字段、叫什么名) MCP inputSchema MCP 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 延迟。它解决的问题是另一个层级的:当订单系统改了一个字段名,你的参数提取器是自动感知,还是运行时静默失败?

好架构是让系统自己保持一致性。

Logo

小龙虾开发者社区是 CSDN 旗下专注 OpenClaw 生态的官方阵地,聚焦技能开发、插件实践与部署教程,为开发者提供可直接落地的方案、工具与交流平台,助力高效构建与落地 AI 应用

更多推荐