这篇想弄清楚几件事:MCP server 配置从哪来、支持哪几种 transport?连接生命周期是怎样的、单个 server 挂掉会怎样?一个 MCP tool 如何变成模型可调的 ?权限系统如何对待 MCP 工具?

MCP 工具进来以后,就是普通工具

MCP(Model Context Protocol)是「外部工具 / 资源 → agent」的标准协议。OpenHarness 做 client:启动时连上配置的 server,把每个 MCP tool 用 包装成普通 注册进 ——从此模型调它和调内置工具走完全相同的管线(schema 校验、权限、hooks)。

例Example text
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 配置观察“有模型、连不上”。

图Fig. 外部能力如何变成普通工具MCP 实验台 · 本地模拟,不启动子进程也不发请求MCP bench · simulated locally, no processes or requests

三个 server · 失败互相隔离Three servers · failures stay isolated

一McpToolAdapter 命名

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

选择 MCP toolPick an MCP tool
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_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

三模型调用路径

调用管线Pipeline等待触发Waiting

尚未发起调用

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

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

源码地图

  • mcp/types.py:三种 server 配置模型,以及 McpToolInfo / McpConnectionStatus。
  • mcp/config.py:合并 settings 与插件的 server 配置。
  • mcp/client.py:,负责连接、调用、重连、关闭。
  • tools/mcp_tool.py:,MCP tool → BaseTool 的桥。
  • tools/list_mcp_resources_tool.py 等:资源列举 / 读取、auth 工具。
  • ui/runtime.py: 里创建并连接 manager;session 结束时 close。
  • commands/registry.py:/mcp 状态命令。

三种 transport,ws 目前连不上

  • stdio:command + args + env + cwd。起本地子进程,走标准输入输出。支持。
  • http:url + headers。streamable HTTP(httpx.AsyncClient)。支持。
  • ws:url + headers。配置模型存在,连接时直接标 failed(Unsupported MCP transport in current build)。
配置能过 ≠ 能连

McpWebSocketServerConfig 能通过配置校验,但 connect_all() 只对 stdio / http 有分支,其余一律标记 failed。魔改加 transport 时要动的是 client.py,不是 types.py。

一个 server 挂了,不连累别人

串行连接每个 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。

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

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

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

MCP tool 如何变成普通工具

做三件事:

  1. 命名:mcp__<server>__<tool>,非法字符替换为 _,首字符非字母加 mcp_ 前缀()。
  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。
权限视角

McpToolAdapter 没有覆盖 is_read_only(),走 BaseTool 默认的 False——所有 MCP 工具一律按「可能有副作用」处理,default 权限模式下每次调用都要用户确认。这是保守且正确的默认:harness 无法知道外部工具是否只读。

一份最小配置

例Example 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)。

结果和错误怎么回到模型

例Example text
模型 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 是一个通用插座标准

MCP 是 AI 应用和外部能力之间的“通用插座标准”。它不让模型直接碰数据库、GitHub 或企业系统;而是让 host 通过统一协议连接 MCP server,发现工具和上下文,再在自己的权限边界内代为调用。它解决的是“每个 AI 客户端都要给每个服务各写一遍适配器”的 M×N 集成问题。

1. 从“万能转接头”到“统一插座”

没有共同协议时:

例Example text
          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 集成运行时协议。

常见分层:

例Example text
模型 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. server 能提供的三样东西

  • 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,远程用 HTTP

  • 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 当通用插座理解即可;回到这篇的源码时,优先抓住四件事:

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

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