s17: MCP Connectors — 外接工具, 标准协议, 信任模型
"外接工具, 标准协议, 信任模型" — MCP 让 agent 的工具池可以扩展;Connector 要受信任,具体工具还要被当前 Skill 显式声明。
Harness 层: 扩展生态 — agent 的外接工具系统。
代码架构图
flowchart LR
A["MCP Config"] --> B["Client Handshake"]
B --> C["Tool Schema Import"]
T["Connector Trust"] --> G{"Trust ∩ Skill Grant"}
P["Skill Permission Grant"] --> G
C --> G
G -->|Allow| D["Tool Dispatch"]
G -->|Deny| X["Permission Error"]
D --> E["Remote Tool Result"]
C -. "state" .-> S["runtime state"]
S -. "recover" .-> C学习前置知识
- MCP 把外部服务能力标准化成 tools/list 和 tools/call。
- 连接器需要信任模型, 不能发现了就自动执行。
- 外部工具同样要进入权限、审计、上下文预算。
本章抓住的 WorkBuddy-style 机制
- 用连接器发现、信任、调用三个阶段模拟 MCP 生命周期。
- 把公开架构研究中的 MCPConnectors 思想泛化为多类连接器。
- 展示 MCP 工具如何合并进统一工具池。
常见误区
- 连接器越多越好是错觉, 工具膨胀会降低选择质量。
- 未信任连接器直接暴露给模型, 风险很高。
- MCP 工具不走统一审计, 会出现治理盲区。
- 信任一个 Connector 不等于允许每个 Skill 调用它的全部工具。
问题
s16 的 Skills 系统让 agent 可以按需加载单个技能。但技能本质上是 prompt 模板——它们告诉 agent "怎么做",而不是提供新的"能做什么"。
如果你的 agent 需要读腾讯文档、操作 GitHub PR、发飞书消息呢?这些不是 prompt 能解决的问题——你需要真正调用外部服务的 API。
当然可以把每个外部服务的调用直接写进 agent 的代码里。但这意味着:每加一个服务就改一次代码、重新发布。40 个服务就是 40 次改动。更糟糕的是,不同团队开发的工具无法复用——每个人都在造自己的 GitHub 集成。
MCP(Model Context Protocol)解决了这个问题。它定义了一个标准协议:任何工具只要实现 tools/list(列出你能做什么)和 tools/call(执行某个工具),就可以被任何 MCP 客户端发现和使用。工具开发者和 agent 开发者彻底解耦。
但"外接工具"带来了一个安全问题:一个恶意连接器可以执行任意命令、窃取数据。第一道门是信任模型——每个连接器必须由用户手动 "Trust" 后才会激活;第二道门是 Skill 权限——已连接工具只有同时出现在审核后的工具白名单中、且允许网络访问时,才会进入延迟工具池。
解决方案
MCP Connector Lifecycle
configured ──► disconnected ──► trusted ──► connected
│ │ │ │
│ │ │ │
mcp.json 写入 进程未启动 用户点击 Trust tools/list 发现
配置已保存 工具池不可见 信任持久化 再与 Skill Grant 求交集
┌──────────────────────────────────────┐
│ Agent Tool Pool │
│ │
│ Built-in Tools MCP Tools │
│ ┌──────────┐ ┌────────────────┐ │
│ │ bash │ │ mcp__github__ │ │
│ │ read │ │ create_pr │ │
│ │ write │ │ mcp__feishu__ │ │
│ │ glob │ │ send_msg │ │
│ │ grep │ │ mcp__notion__ │ │
│ │ ... │ │ search │ │
│ └──────────┘ └────────────────┘ │
└──────────────────────────────────────┘| 阶段 | 含义 | 工具可见性 |
|---|---|---|
| configured | 配置写入 mcp.json | 不可见 |
| disconnected | 进程未启动 | 不可见 |
| trusted | 用户已信任,进程待启动 | 不可见 |
| connected | 进程启动,tools/list 完成 | 仅已声明工具可见(延迟加载) |
MCP 协议核心方法:
| 方法 | 方向 | 用途 |
|---|---|---|
tools/list | client → server | 发现可用工具 |
tools/call | client → server | 调用某个工具 |
resources/list | client → server | 列出可用资源 |
resources/read | client → server | 读取资源内容 |
工作原理
连接器配置
WorkBuddy 的连接器配置在 mcp.json 中:
{
"mcpServers": {
"github": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-github"],
"env": { "GITHUB_TOKEN": "ghp_xxx" }
},
"feishu": {
"command": "npx",
"args": ["-y", "@workbuddy/mcp-feishu"],
"env": { "FEISHU_APP_ID": "cli_xxx", "FEISHU_APP_SECRET": "xxx" }
}
}
}每个连接器是一个独立的子进程,通过 stdio 与 agent 通信。配置解析后,连接器进入 disconnected 状态。
信任模型
TRUST_FILE = Path.home() / ".workbuddy" / "connector_trust.json"
def is_trusted(connector_name: str) -> bool:
"""检查连接器是否已被用户信任。"""
trust_data = json.loads(TRUST_FILE.read_text()) if TRUST_FILE.exists() else {}
return trust_data.get(connector_name, {}).get("trusted", False)
def trust_connector(connector_name: str):
"""用户手动信任一个连接器。"""
trust_data = json.loads(TRUST_FILE.read_text()) if TRUST_FILE.exists() else {}
trust_data[connector_name] = {"trusted": True, "timestamp": time.time()}
TRUST_FILE.write_text(json.dumps(trust_data, indent=2))信任状态持久化在 ~/.workbuddy/connector_trust.json。未信任的连接器不会启动进程,其工具不会出现在工具池中。
信任只决定 Connector 进程能否启动,不决定某个 Skill 能调用哪些工具。教学实现使用独立的 MCPPermissionGrant:
grant = MCPPermissionGrant(
tools={"mcp__github__list_issues"},
network=True,
)
manager = ConnectorManager(MCP_CONFIG, grant)省略 grant 时默认 NO_MCP_PERMISSIONS,即使 Connector 已受信任也不暴露任何工具。更新或撤销 grant 会清空已加载 schema;执行阶段还会重新检查,避免“先加载、后撤权、仍可执行”的 TOCTOU 缺口。
工具发现
连接器被信任并启动后,agent 发送 tools/list 请求:
def discover_tools(connector_name: str) -> list[dict]:
"""通过 MCP 协议发现连接器提供的工具。"""
connector = connectors[connector_name]
response = connector.request("tools/list", {})
discovered = []
for tool in response.get("tools", []):
# 命名空间化: mcp__connectorname__toolname
namespaced_name = f"mcp__{connector_name}__{tool['name']}"
if not active_skill_grant.allows(namespaced_name):
continue
discovered.append({
"name": namespaced_name,
"description": tool["description"],
"input_schema": tool["inputSchema"],
})
return discovered工具名被命名空间化为 mcp__connectorname__toolname,避免与内置工具或其他连接器的工具冲突。
延迟工具加载(Deferred Tools)
MCP 工具的 schema 不会一开始就全部加载到系统提示中。它们是"延迟工具"——只有当 agent 需要时才通过 ToolSearch 加载 schema,然后通过 DeferExecuteTool 执行:
# 系统提示中只告诉 agent 有哪些延迟工具可用
# 不包含完整的 schema
DEFERRED_TOOLS = [
{"name": "mcp__github__create_pr", "description": "Create a GitHub PR"},
{"name": "mcp__feishu__send_message", "description": "Send a Feishu message"},
# ... 只有 name + description,没有 input_schema
]
# 当 agent 决定使用某个延迟工具时:
# 1. ToolSearch 加载 schema
schema = tool_search("mcp__github__create_pr") # 再检查 Skill grant
# 2. DeferExecuteTool 执行
result = defer_execute_tool("mcp__github__create_pr", {"title": "...", "body": "..."}) # 再检查这个设计解决了"工具爆炸"问题——40 个连接器 × 每个 10 个工具 = 400 个工具。如果把 400 个工具的完整 schema 全塞进系统提示,会浪费大量 token。延迟加载让 agent 只在需要时付出代价。
连接器状态注入
连接器的状态会被注入到系统提示中,让 agent 知道哪些工具可用:
def build_connector_context() -> str:
"""构建连接器状态上下文,注入系统提示。"""
lines = ["<available_deferred_tools>"]
for name, conn in connectors.items():
if conn.status == "connected":
for tool in conn.tools:
if not active_skill_grant.allows(tool["name"]):
continue
lines.append(f"- {tool['name']}: {tool['description']}")
lines.append("</available_deferred_tools>")
return "\n".join(lines)WorkBuddy 架构对照
生产级桌面 agent 的 MCP 连接器实现分布在多个层面:
配置文件
连接器配置在两个位置:
- 用户级:
~/.workbuddy/mcp.json - 项目级:
connectors/default/mcp.json
{
"mcpServers": {
"tencent-docs": {
"command": "npx",
"args": ["-y", "@workbuddy/mcp-tencent-docs"],
"env": { "TENCENT_DOCS_TOKEN": "..." }
}
}
}连接器管理
生产级 Sidecar 通常负责连接器的生命周期管理。每个连接器是一个独立的 Node.js 子进程,通过 stdio 通信。连接器状态通过 RPC 暴露给渲染器进程,在设置界面显示。
信任模型实现
用户在设置界面看到新连接器时,需要点击 "Trust" 按钮。这个动作会:
- 将信任状态写入持久化存储
- 启动连接器子进程
- 执行
tools/list发现工具 - 与当前 Skill 的工具/网络 grant 求交集
- 只把交集注册到延迟工具池
未信任的连接器显示为 "Not Trusted" 状态,其工具完全不可见。已信任但未被当前 Skill 声明的工具同样不可见,也不能通过直接构造 DeferExecuteTool 调用绕过。
延迟工具加载
WorkBuddy 的系统提示中包含 <available_deferred_tools> 块,列出所有已连接连接器的工具名称和简短描述。完整的 input_schema 不包含在提示中。
当 agent 需要使用某个 MCP 工具时,系统提示指示它:
- 使用
ToolSearch加载工具的 schema - 使用
DeferExecuteTool执行工具
这与内置的延迟工具(如 ImageGen、LSP)使用相同的机制。
连接器生态
WorkBuddy 内置 连接器生态:
| 连接器 | 功能 |
|---|---|
tencent-docs | 腾讯文档读写 |
github | GitHub PR/Issue 操作 |
feishu | 飞书消息/文档 |
notion | Notion 页面管理 |
dingtalk | 钉钉消息/审批 |
wecom | 企业微信消息 |
ardot | AR 设计工具集成 |
代码 walkthrough
code.py 模拟了 MCP 连接器的完整生命周期:
- 连接器配置解析 — 从 JSON 配置加载连接器定义
- 信任模型 — 模拟用户信任/拒绝连接器的交互
- 工具发现 — 模拟
tools/list协议方法 - 延迟工具加载 — 模拟
ToolSearch+DeferExecuteTool模式 - 命名空间化 — 工具名格式为
mcp__connectorname__toolname - 权限交集 — Connector trust、网络许可和 Skill 工具白名单缺一不可
- 双重检查 —
ToolSearch与DeferExecuteTool都 fail closed - 状态注入 — 只将允许的连接器工具注入系统提示
内置模拟连接器:
github— 模拟 GitHub PR 创建feishu— 模拟飞书消息发送notion— 模拟 Notion 页面搜索
运行
python s17_mcp_connectors/code.py
python -m pytest -q tests/test_skill_permissions.py试试这些 prompt:
List all available connectors(查看连接器状态)Trust the github connector(模拟信任流程)Create a PR on GitHub to add dark mode(使用 MCP 工具)Send a message on Feishu saying hello(使用另一个连接器)
观察重点:未信任的连接器工具不可见;信任后还要通过显式 Skill grant。交集中的工具先经 ToolSearch 加载 schema,再经 DeferExecuteTool 二次校验并执行。
练习
- 添加一个新的模拟连接器(如
dingtalk),包含send_work_message工具。观察命名空间化如何避免冲突。 - 实现
resources/list和resources/read的模拟,让连接器可以暴露"资源"(如文档列表)而不仅是工具。 - 实现连接器健康检查:如果一个连接器进程崩溃,自动将其状态回退到
disconnected,并从工具池中移除其工具。
下一课
s17 让 agent 可以接入无限的外部工具。但工具是"点"——它们提供单个操作。如果你需要让 agent 整体变成某个领域的专家呢?比如让它像一个软件公司的架构师那样思考,或者像一个趋势研究员那样分析?
s18 Experts System → 专家包,整包加载领域知识。