本节摘要:插件架第二层:MCP(Model Context Protocol)设置页。如果说 Skills 教 agent「怎么做」,MCP 就是给 agent「发工具」——通过一个标准化协议,任意外部 MCP server(GitHub、Slack、数据库、浏览器……)都能把一组工具暴露给 agent 后端。本节从
MCPServerConfig类型出发,走读src/components/features/mcp-page/(约 1904 行)的市场浏览、安装表单与已装服务器管理,再精读src/api/mcp-service/mcp-service.api.ts的连通性测试(stdio/SSE 两种传输、凭据替换与脱敏、OAuth 授权流程),最后厘清 MCP 在 Canvas 架构中的定位:配置在 Canvas、连接在 agent-server、受益者是会话里的 agent。
内容来源:src/types/mcp-server.ts、src/api/mcp-service/mcp-service.api.ts、src/components/features/mcp-page/、specs/mcp-settings.md。
⚠️ 注意:MCP server 由 agent-server 进程连接(stdio 是它的子进程,SSE/SHTTP 是它的网络连接),浏览器从不直连 MCP server。Canvas 做的是「配置编辑器+连通性体检」,真正跑工具的是后端——这决定了下文所有代码的形状。
MCPServerConfig 的字段与配置表单的生成方式。testServer 的本地执行、云短路、凭据替换与脱敏后处理。MCP(Model Context Protocol)是一个开放标准,定义「工具提供方(server)」与「工具使用方(client,通常是 agent 宿主)」之间的契约:server 声明自己有哪些工具(名称、JSON Schema 参数、描述),client 按需调用并取回结果。它的意义与第 4 章的 ACP 同构——ACP 标准化了「agent 与客户端」,MCP 标准化了「工具与 agent」——两者叠加,agent 的能力生态变成乐高:换 agent 不换工具,换工具不换 agent。
传输层有三种形态(MCPServerType):
npx -y @some/mcp-server),经标准输入输出对话——适合本地工具;对 Canvas 而言三者只是配置形状不同:stdio 填命令+参数+环境变量,远程填 URL+请求头(+认证)。
// src/types/mcp-server.ts(全文节选) export type MCPServerType = "sse" | "stdio" | "shttp"; export interface MCPServerConfig { id: string; type: MCPServerType; name?: string; url?: string; // 远程传输(SSE/SHTTP)的端点 headers?: Record<string, string>; // 远程传输的自定义请求头 timeout?: number; command?: string; // stdio 传输的启动命令 args?: string[]; // stdio 传输的命令参数 env?: Record<string, string>; // stdio 传输的环境变量(常为凭据) auth?: MCPAuthCredential; // api_key/bearer/oauth2 认证 /** * agent 是否可使用该 server。缺省即启用——只有显式 false 才禁用, * 因此存量配置不受影响。 */ enabled?: boolean; }
两个字段设计有讲究:env 是 stdio server 的凭据通道(API key 经环境变量注入子进程,与第 4 章 ACP 的 secret-as-env 同一思路);enabled 采用「缺省即启用」的宽松默认并显式注释动机——只有 false 才禁用,保证存量配置在字段引入前后行为不变。这个配置最终落在 agent settings 的 mcp_config 上(第 4 章的 AgentKind 注释已埋过伏笔:ACP 模式下 mcp_config 依然生效——外部 agent 也能挂 MCP 工具)。
src/components/features/mcp-page/(约 1904 行)的文件清单就是一张功能地图:marketplace-section+marketplace-card 渲染集成市场——条目来自 @openhands/extensions/integrations 的 INTEGRATION_CATALOG(第 5 章推荐自动化卡片同款来源,第 1 节预告过);installed-servers-section+installed-server-card 管理已装服务器;custom-server-editor 支持手写任意 MCP server 配置;mcp-server-health 显示健康状态;mcp-toolbar+mcp-section-filter* 提供搜索与分类过滤;mcp-logo-stack-badge 拼装品牌 logo。
核心是 install-server-modal.tsx(735 行)——配置表单不是手写的,是从市场条目生成的:
// src/components/features/mcp-page/install-server-modal.tsx(节选) function getRemoteHeaderFields( option: McpMarketplaceConnectionOption | undefined, ): MarketplaceField[] { if (option?.transport.kind !== "shttp" && option?.transport.kind !== "sse") { return []; // stdio 传输没有请求头字段 } return option.transport.headerFields ?? []; } function makeInitialState(entry: MarketplaceEntry): FieldState { const values: Record<string, string> = {}; const savedAsSecret: Record<string, boolean> = {}; const option = getInstallableMcpConnectionOption(entry); const template = option?.transport; if (template?.kind === "stdio") { for (const field of template.envFields ?? []) { values[field.key] = ""; // 密码字段预勾选"存为 secret";非密码字段默认关闭 } } // ... }
市场条目自带「连接选项」描述:stdio 条目声明 envFields(要收集哪些环境变量、哪些是密码),远程条目声明 headerFields 与认证策略(api_key/bearer/oauth2)。表单按描述渲染,密码字段默认勾选**「存为 secret」**(save-as-secret-toggle)——凭据不落明文配置,而是进 agent-server 的 secret store,配置里只留引用,读取时再替换。isCredentialOptional 决定凭据是否可留空(有些 server 本地匿名可用)。这套「目录描述表单」的模式与第 5 章 setup 清单同宗:数据与渲染分离,新增集成零前端改动。
装完之后的管理面在 installed-servers-section+installed-server-card:每张卡片显示连接形态(命令或 URL)、启停开关(写回 enabled 字段)、编辑入口与实时健康徽标。健康探测由 src/api/mcp-health/probe-mcp-server-health.ts 驱动——安装成功时 seedMcpServerHealth() 立即播种一次初始状态,后台周期性复测;mcp-server-health.tsx 把「正常/失联/凭据失败」画成不同颜色。findInstalledEntryMatch() 则负责反向对账:市场条目与已装配置互相匹配(marketplace-card 据此显示「已安装」角标),手动改过的自定义服务器不会误配到任何市场条目上。
保存前要「体检」:连得上吗?工具列表是什么?凭据有效吗?McpService.testServer() 是答案,但它的第一段代码就颠覆直觉:
// src/api/mcp-service/mcp-service.api.ts(节选) static async testServer(server: MCPServerConfig): Promise<ExtendedMCPTestResponse> { // MCP 连通性测试端点在本地 agent-server 上。它从该进程的环境里 // spawn 配置的 stdio 命令/发起 SSE 或 SHTTP 连接。云后端不把该端点 // 暴露给前端——MCP server 实际会跑在云沙箱里,用户开会话前浏览器 // 根本够不到它。此处若对云会话调 getAgentServerClientOptions() 会抛 // NoBackendAvailableError 并卡死整个安装流程。短路返回合成成功, // 让保存继续;真实连接失败会在会话运行时暴露。 if (getActiveBackend().backend.kind === "cloud") { return { ok: true, tools: [] }; } const { host, apiKey } = getAgentServerClientOptions(); const client = new MCPClient({ host, ...(apiKey ? { apiKey } : {}) }); try { const { request, substituted } = await buildMcpTestRequest(server); const result = (await client.testServer(request)) as ExtendedMCPTestResponse; return finalizeMcpTestResponse(result, validation, [substituted, server]); } finally { client.close(); } }
「云后端返回合成成功」是精心权衡的谎言:云沙箱里的 MCP server 浏览器测不到,硬测只会报错卡流程;不如放行保存,把真相留给会话运行时。测试请求的构造里藏着凭据的处理链:buildMcpTestRequest() 先 substituteRedactedMcpCredentials() 把配置里的 secret 引用换回真实值(配置在 UI 上是被脱敏的),再把脱敏前后的两份配置都传给 finalizeMcpTestResponse()——错误信息与工具结果要对着两份配置做秘密擦除(redactMcpSecrets),防止 API key 泄漏进错误提示。finalize 还做一层「凭据校验」:如果目录声明了探测工具调用(如列一次仓库),且 server 确实公示了该工具,则把失败的只读调用翻译成 error_kind: "credentials"——但注释强调,只有工具确实出现在 tools 列表里才这么判:一个不暴露该工具的 server 变体(比如 hosted 版)必须降级为「连通成功」,而不是误报「凭据错误」。
OAuth 是最重的一段:authorizeOAuth() 先开一个空白弹窗占位(避免浏览器拦截后开的窗口),startOAuth 拿到 job_id+authorization_url,轮询状态直到 callback_ready 才把弹窗导航到授权页,随后以每秒一次的频率轮询最多 120 秒(OAUTH_MCP_TEST_TIMEOUT_SECONDS),等用户在弹窗里完成授权、状态变成 succeeded/failed。注释顺带交代了一个类型边界的无奈:typescript-client 把 oauth_state 的递归 JSON 类型生成为 unknown,线上形状没变,本地收窄回来即可。
把 MCP 放回全书坐标系:
MCPServerConfig[],市场表单降低门槛,secret 机制保凭据安全;mcp_config 建立;顺带做一次跨库对照,巩固概念:在 smolagents 那类 Python agent 库里,「给 agent 加能力」往往意味着写一个 Python 工具类注册进 agent(Tool 子类、@tool 装饰器);MCP 把这件事从「宿主内编码」变成「跨进程协议」——server 与 agent 可以由不同人、不同语言实现,靠标准消息面协作。代价是多了一层连接管理(这正是 mcp-page 存在的理由),收益是工具生态的即插即用。Canvas 选择了协议路线,也就要承担协议路线的工程成本:传输差异、凭据安全、连通性体检、OAuth 流程——本节走读的每一块代码都是在付这笔「标准化税」。
💡 驾驶舱要点:MCP 设置页最大的工程亮点是诚实的边界感——云后端测不到就明说「留到会话运行时」,探测工具没公示就不误报凭据错误,错误信息先脱敏再展示。给不可靠的外部世界做 UI,宁可少下结论,不可错下结论。
MCPServerConfig:stdio 填 command/args/env,远程填 url/headers/auth;enabled 缺省即启用以兼容存量;配置落 agent settings 的 mcp_config,ACP 会话同样可用。INTEGRATION_CATALOG,安装表单由条目的 envFields/headerFields/认证策略生成;密码字段默认「存为 secret」,配置留引用、用时替换。testServer:本地后端经 agent-server 真连真测;云后端短路返回合成成功(沙箱里的连接浏览器够不到);测试链路完成凭据替换→调用→双份配置脱敏→凭据错误分类;OAuth 走「预开弹窗+轮询 callback_ready+最长 120 秒」状态机。下一节:
03 Canvas Extensions 扩展体系——插件架第三层,也是最强的一层:改变 App 本身。