MCP 协议实践 —— 让 Skill 体系从“私有胶水“走向“标准协议“
2026/6/26 5:05:53 网站建设 项目流程

一个 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-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 更新不保证调用端同步感知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 消除同语义字段的重复。核心设计是三层解耦:

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

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询