← 返回文章

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。

失败隔离 · 全量重连
01

McpToolAdapter 命名

连接成功后,每个 MCP tool 变成 mcp__server__tool 并进入 ToolRegistry。

server
filesystem
original tool
read_file
adapted name
mcp__filesystem__read_file
is_read_only
False(BaseTool 默认 · 一律当有副作用)
02

Registry 快照

只有 connected 的 server 会贡献工具。挂掉一个不影响其他。

  • mcp__filesystem__read_filefilesystem · read_file
  • mcp__filesystem__write_filefilesystem · write_file
  • mcp__filesystem__list_directoryfilesystem · list_directory
  • mcp__internal-api__search_ordersinternal-api · search_orders
  • mcp__internal-api__create_ticketinternal-api · create_ticket
03

模型调用路径

PIPELINE等待触发

尚未发起调用

先选一个已连接 server 上的 tool,再点「模拟模型调用」。

  1. 模型看到的工具名来自 ToolRegistry.to_api_schema()
  2. 名字形态:mcp__<server>__<tool>
  3. 权限默认按有副作用处理(is_read_only=False)

核心文件

三种 transport(一种目前不落地)

连接生命周期:失败是隔离的

connect_all() 串行连接每个 server,每个都包在独立 try/except + 独立 AsyncExitStack 里:

  1. 一个 server 连不上 → 状态标 failed(带原因 detail),不影响其他 server,也不阻断 CLI 启动。
  2. 连接成功的标志:initialize() 握手 → list_tools()list_resources()(后者容忍 Method not found——server 可以没有 resources)。
  3. 会话结束:close() 逐个 aclose 栈,吞掉 RuntimeError / CancelledError

重连语义:全量,不是单点

reconnect_all() = close() + 重置所有状态 + connect_all()。触发点是 McpAuthTool(保存新 auth 后重连生效)。

注意:调用单个工具失败时不会自动重连——call_tool 直接把异常包成 McpServerNotConnectedError 返回错误结果,模型下一轮自行决定怎么办。

适配器:MCP tool 如何变成普通工具

McpToolAdapter 做三件事:

  1. 命名mcp__<server>__<tool>,非法字符替换为 _,首字符非字母加 mcp_ 前缀(_sanitize_tool_segment)。
  2. 动态 input_model:用 Pydantic create_model 从 server 报告的 JSON Schema 生成校验模型——required 字段必填,其余 Optional。类型映射是浅层的:string / integer / number / boolean / array / object 六种,嵌套约束(enum、pattern、嵌套 properties)会丢失。
  3. 转发调用execute()manager.call_tool(server, tool, args) → 把结果里的 text 片段拼接返回;非 text 内容序列化成 JSON。

最小配置模板

settings.mcp_servers · conceptualjson
{
"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) → 作为错误工具结果回给模型

和前几课拼起来:

推荐精读(一手源码)

主读:src/openharness/mcp/client.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
        │
        ▼
Host / Agent harness
        │  MCP:tools/list、tools/call、resources/read ...
        ▼
MCP server
        │  内部再调 REST / SQL / SDK
        ▼
业务 API / 数据库 / SaaS

2. 三个角色:host、client、server

3. 协议说什么:JSON-RPC 2.0

从较新的稳定规范起,JSON-RPC batch 已被移除;实现端不要再假设批量消息可用。

4. initialize 是能力协商,不是客套

连接大致经历初始化、运行、关闭。初始化时双方交换 protocolVersioncapabilities:server 声明是否有 tools / resources / prompts / logging;client 声明是否提供 sampling、elicitation、roots 等。

只因某个方法在规范里存在,并不表示对端一定开启它。初始化完成前,双方对普通请求都有限制。候选版规范可能讨论更“无会话”的方向,但生产实现仍应以双方实际协商到的稳定版本为准。

5. 三种正向原语

Tools 不是 resources。tool 是“做一件事”;resource 主要是“给上下文”。较新规范还允许 tool 声明 outputSchema、返回 structuredContent,让 host 不必只从自然语言里猜 JSON。

6. 反向能力:server 也可以请 host 帮忙

心智模型:server 可以请求,host 保留批准、展示、选模型、持密钥、定政策的权力。

7. 传输层

OpenHarness 当前落地的是 stdio + httpws 仅有配置壳。

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 语法错,而是模型会读到不可信文字,然后被诱导去调用有权限的工具。

应对方向:最小权限、审批、allowlist、审计、锁定版本与发布者、对外写操作强制确认、隔离进程、不要为了方便把整个 home 目录授权出去。

10. 读 OpenHarness 时的取舍

把 MCP 当通用插座理解即可;落回本课源码时,优先抓住四件事:

  1. 配置从哪合并;
  2. 哪些 transport 真能连;
  3. 失败如何隔离;
  4. 适配后如何复用 Tools / Permissions / Hooks。

规范会继续演进(远程 transport、结构化输出、OAuth 细节、实验性 tasks 等)。OpenHarness 的价值是:在模型与外部世界之间,放一层可观察、可权限化、可失败隔离的 client。