SOURCE NOTE · MCP SYSTEM
Lesson 7: MCP 子系统
MCP 把外部工具与资源接到 agent。OpenHarness 做 client:连上 server,用适配器把每个 MCP tool 注册成普通 BaseTool,再走同一套管线。
学完本课,你能回答:MCP server 配置从哪来、支持哪几种 transport?连接生命周期是怎样的、单个 server 挂掉会怎样?一个 MCP tool 如何变成模型可调的 mcp__server__tool?权限系统如何对待 MCP 工具?
一句话概括
MCP(Model Context Protocol)是「外部工具 / 资源 → agent」的标准协议。OpenHarness 做 client:启动时连上配置的 server,把每个 MCP tool 用 McpToolAdapter 包装成普通 BaseTool 注册进 ToolRegistry——从此模型调它和调内置工具走完全相同的管线(schema 校验、权限、hooks)。
settings.mcp_servers + 插件 mcp_servers(命名空间 plugin:name)
|
v
load_mcp_server_configs() 合并
|
v
McpClientManager.connect_all() ← build_runtime() 时
stdio:起子进程 http:streamable HTTP
每个 server:initialize → list_tools → list_resources
|
v
每个 MCP tool → McpToolAdapter → 注册为 mcp__<server>__<tool>
|
v
模型调用 → 普通工具管线(校验 → 权限 → hooks)→ session.call_tool
下面的 MCP Lab 把「连接隔离 / 命名适配 / 调用管线」放在一张实验台上:你可以挂掉某个 server,看其他 server 是否仍注册工具;也可以对 ws 配置观察“有模型、连不上”。
MCP LAB · LOCAL SIMULATION
外部能力如何变成普通工具
选 server、看连接隔离、观察命名适配与调用管线。组件不启动真实子进程,也不发起 HTTP。
McpToolAdapter 命名
连接成功后,每个 MCP tool 变成 mcp__server__tool 并进入 ToolRegistry。
- server
- filesystem
- original tool
- read_file
- adapted name
mcp__filesystem__read_file- is_read_only
- False(BaseTool 默认 · 一律当有副作用)
Registry 快照
只有 connected 的 server 会贡献工具。挂掉一个不影响其他。
mcp__filesystem__read_filefilesystem · read_filemcp__filesystem__write_filefilesystem · write_filemcp__filesystem__list_directoryfilesystem · list_directorymcp__internal-api__search_ordersinternal-api · search_ordersmcp__internal-api__create_ticketinternal-api · create_ticket
模型调用路径
尚未发起调用
先选一个已连接 server 上的 tool,再点「模拟模型调用」。
- 模型看到的工具名来自 ToolRegistry.to_api_schema()
- 名字形态:mcp__<server>__<tool>
- 权限默认按有副作用处理(is_read_only=False)
核心文件
mcp/types.py:三种 server 配置模型,以及McpToolInfo/McpConnectionStatus。mcp/config.py:合并 settings 与插件的 server 配置。mcp/client.py:McpClientManager,负责连接、调用、重连、关闭。tools/mcp_tool.py:McpToolAdapter,MCP tool → BaseTool 的桥。tools/list_mcp_resources_tool.py等:资源列举 / 读取、auth 工具。ui/runtime.py:build_runtime()里创建并连接 manager;session 结束时close。commands/registry.py:/mcp状态命令。
三种 transport(一种目前不落地)
- stdio:
command + args + env + cwd。起本地子进程,走标准输入输出。支持。 - http:
url + headers。streamable HTTP(httpx.AsyncClient)。支持。 - ws:
url + headers。配置模型存在,连接时直接标failed(Unsupported MCP transport in current build)。
连接生命周期:失败是隔离的
connect_all() 串行连接每个 server,每个都包在独立 try/except + 独立 AsyncExitStack 里:
- 一个 server 连不上 → 状态标
failed(带原因 detail),不影响其他 server,也不阻断 CLI 启动。 - 连接成功的标志:
initialize()握手 →list_tools()→list_resources()(后者容忍Method not found——server 可以没有 resources)。 - 会话结束:
close()逐个aclose栈,吞掉RuntimeError/CancelledError。
重连语义:全量,不是单点
reconnect_all() = close() + 重置所有状态 + connect_all()。触发点是 McpAuthTool(保存新 auth 后重连生效)。
注意:调用单个工具失败时不会自动重连——call_tool 直接把异常包成 McpServerNotConnectedError 返回错误结果,模型下一轮自行决定怎么办。
适配器:MCP tool 如何变成普通工具
McpToolAdapter 做三件事:
- 命名:
mcp__<server>__<tool>,非法字符替换为_,首字符非字母加mcp_前缀(_sanitize_tool_segment)。 - 动态 input_model:用 Pydantic
create_model从 server 报告的 JSON Schema 生成校验模型——required字段必填,其余Optional。类型映射是浅层的:string/integer/number/boolean/array/object六种,嵌套约束(enum、pattern、嵌套 properties)会丢失。 - 转发调用:
execute()→manager.call_tool(server, tool, args)→ 把结果里的 text 片段拼接返回;非 text 内容序列化成 JSON。
最小配置模板
{
"mcp_servers": {
"filesystem": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "D:\\data"],
"env": {}
},
"internal-api": {
"type": "http",
"url": "https://mcp.internal.example.com/mcp",
"headers": { "Authorization": "Bearer ..." }
}
}
}插件提供的 server 会以 插件名:server名 的命名空间合并进来(setdefault——settings 侧键不同所以通常不冲突;同名冲突时以合并规则为准,阅读 mcp/config.py)。
结果与错误的路径
模型 tool_use: mcp__filesystem__read_file {...}
→ Pydantic 校验(动态生成的 input_model)
→ PermissionChecker(is_read_only=False → default 模式需确认)
→ pre_tool_use hooks
→ McpToolAdapter.execute()
→ manager.call_tool() → session.call_tool()
→ 结果: text 片段拼接 / structuredContent / "(no output)"
任一环节失败 → McpServerNotConnectedError
→ ToolResult(is_error=True) → 作为错误工具结果回给模型
和前几课拼起来:
- Tools:MCP 不另开一条执行宇宙,它复用 BaseTool / Registry。
- Permissions:MCP 默认更保守,因为宿主看不见外部实现是否只读。
- Hooks:
pre_tool_use/post_tool_use对mcp__...名字同样生效,可用 matcher 做组织策略。 - Skills:Skills 给剧本;MCP 给可调用的外部手。
推荐精读(一手源码)
主读:src/openharness/mcp/client.py —— 连接、失败隔离、调用与重连。
接着看:
src/openharness/mcp/types.py、config.py—— 配置形状与合并;src/openharness/tools/mcp_tool.py—— 命名、动态 schema、转发;src/openharness/ui/runtime.py—— 何时connect_all/close;- 权限与工具主循环:
permissions/checker.py、engine/query.py。
拓展:MCP 是什么?
一句话总结
MCP 是 AI 应用和外部能力之间的“通用插座标准”。它不让模型直接碰数据库、GitHub 或企业系统;而是让 host 通过统一协议连接 MCP server,发现工具和上下文,再在自己的权限边界内代为调用。它解决的是“每个 AI 客户端都要给每个服务各写一遍适配器”的 M×N 集成问题。
1. 从“万能转接头”到“统一插座”
没有共同协议时:
GitHub Notion 数据库 工单系统
| | | |
Claude Code ├─────────┼─────────┼──────────┤ 各写一套
OpenHarness ├─────────┼─────────┼──────────┤ 再写一套
IDE Agent ├─────────┼─────────┼──────────┤ 还写一套
MCP 把中间那层约定成插座:服务方实现一次 MCP server;host(桌面应用、IDE、agent harness)实现一次 MCP client。理想情况下连接数从 M×N 缩到 M+N。它不保证“接上就安全、接上就好用”,但把怎么连、能发现什么、怎样请求、怎样授权统一了。
它和另外两者不互相替代:
- Function calling / tool use:模型提出“调用哪个函数、传什么 JSON”——模型 ↔ host 的意图层。
- OpenAPI:REST API 的接口说明书——业务 HTTP 合同。
- MCP:host 与能力提供者之间的会话、发现、调用、上下文与授权规则——AI 集成运行时协议。
常见分层:
模型 function calling
│
▼
Host / Agent harness
│ MCP:tools/list、tools/call、resources/read ...
▼
MCP server
│ 内部再调 REST / SQL / SDK
▼
业务 API / 数据库 / SaaS
2. 三个角色:host、client、server
- Host:用户真正使用的 AI 应用。拥有 UI、模型选择、用户同意、凭据、日志和最终执行政策。它不等于模型。
- Client:host 为每个 server 建立的协议连接。握手、收发 JSON-RPC、维护协商能力;多个 client 相互隔离。
- Server:向 client 暴露能力的适配层。可以是本机子进程,也可以是远程 HTTPS 服务。
3. 协议说什么:JSON-RPC 2.0
- Request:有
id,期待回应(如tools/list、tools/call)。 - Response:带同一个
id,返回结果或错误。 - Notification:没有
id,不等回复(如notifications/initialized、工具 / 资源变化、日志)。
从较新的稳定规范起,JSON-RPC batch 已被移除;实现端不要再假设批量消息可用。
4. initialize 是能力协商,不是客套
连接大致经历初始化、运行、关闭。初始化时双方交换 protocolVersion 与 capabilities:server 声明是否有 tools / resources / prompts / logging;client 声明是否提供 sampling、elicitation、roots 等。
只因某个方法在规范里存在,并不表示对端一定开启它。初始化完成前,双方对普通请求都有限制。候选版规范可能讨论更“无会话”的方向,但生产实现仍应以双方实际协商到的稳定版本为准。
5. 三种正向原语
- Tools:可按参数触发的“按钮”。
tools/list、tools/call。可能有副作用。 - Resources:可读取的“资料柜”。
resources/list、resources/read,也可有 URI 模板与订阅。 - Prompts:server 打包好的工作模板。
prompts/list、prompts/get。
Tools 不是 resources。tool 是“做一件事”;resource 主要是“给上下文”。较新规范还允许 tool 声明 outputSchema、返回 structuredContent,让 host 不必只从自然语言里猜 JSON。
6. 反向能力:server 也可以请 host 帮忙
- Sampling:server 请 host 用其选定模型生成 / 判断;密钥与批准仍在 host。
- Elicitation:server 请用户补字段或确认;用户可接受、拒绝、取消。敏感流程应走明确的 URL 模式并征得同意。
- Roots:server 询问自己被允许看到哪些工作区根;不能默认可读整个磁盘。
- Logging:server 把日志递给 host 的可观察性系统;host 负责级别、脱敏与存留。
心智模型:server 可以请求,host 保留批准、展示、选模型、持密钥、定政策的权力。
7. 传输层
- stdio:本机首选。host 启子进程,JSON-RPC 走 stdin/stdout;日志应走 stderr。同时把“启动任意命令”当成高权限行为。
- Streamable HTTP:远程首选。一个 HTTP endpoint;可回普通 JSON,也可用 SSE 流式返回。配合 HTTPS 与 OAuth。
- 旧 HTTP+SSE:历史兼容;新 server 不要默认走这条。
OpenHarness 当前落地的是 stdio + http;ws 仅有配置壳。
8. 远程授权:OAuth 不是装饰
本地 stdio 常由“谁能启动进程、进程拿到哪些环境变量”决定身份;远程 HTTP 需要 token。MCP 授权规范以 OAuth 2.1 为基础:server 是受保护资源,client 持有面向该资源的 bearer token。
落地翻译:不要把“把 Authorization: Bearer ... 配进 MCP URL”当成完整 OAuth 支持。还要处理 token 刷新、401、metadata discovery、scope 与 audience 校验。OAuth 也不会自动防住 prompt injection 或恶意工具描述。
9. 安全:标准化 ≠ 自动安全
最重要的风险不是 JSON-RPC 语法错,而是模型会读到不可信文字,然后被诱导去调用有权限的工具。
- Tool poisoning:恶意 server 在名称、描述、schema 或结果里夹带诱导指令。
- Rug pull:你批准过的工具稍后悄悄换了描述或行为。
- 间接 prompt injection:网页 / issue / 文档 / tool 输出里藏着越权指令。
- 租户隔离错误:server 业务鉴权写错,跨组织数据暴露——协议不替你修。
- 本地执行与供应链:“装一个 stdio server”本质是让 host 启本机程序。
应对方向:最小权限、审批、allowlist、审计、锁定版本与发布者、对外写操作强制确认、隔离进程、不要为了方便把整个 home 目录授权出去。
10. 读 OpenHarness 时的取舍
把 MCP 当通用插座理解即可;落回本课源码时,优先抓住四件事:
- 配置从哪合并;
- 哪些 transport 真能连;
- 失败如何隔离;
- 适配后如何复用 Tools / Permissions / Hooks。
规范会继续演进(远程 transport、结构化输出、OAuth 细节、实验性 tasks 等)。OpenHarness 的价值是:在模型与外部世界之间,放一层可观察、可权限化、可失败隔离的 client。