8.2 MCP Client 实现

模型: claude-opus-4-6 (anthropic/claude-opus-4-6) 生成日期: 2026-02-17


在上一节中,我们了解了 MCP 协议的核心概念和三种传输方式。本节将深入 OpenCode 的 mcp/index.ts 源码,详细分析 MCP Client 的实现——从客户端初始化、连接管理、状态机设计,到 MCP 工具如何被转换为 OpenCode 内部的 Tool 格式。

8.2.1 mcp/index.ts 源码解析

mcp/index.ts 是 OpenCode MCP 模块的核心文件,共 935 行代码。它以一个 MCP 命名空间(namespace)的形式组织所有功能。让我们从依赖导入开始理解其架构。

依赖关系

// AI SDK - 用于将 MCP 工具转换为 AI SDK 格式
import { dynamicTool, type Tool, jsonSchema, type JSONSchema7 } from "ai"

// MCP SDK - 官方的 MCP 客户端库
import { Client } from "@modelcontextprotocol/sdk/client/index.js"
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js"
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"
import { UnauthorizedError } from "@modelcontextprotocol/sdk/client/auth.js"
import {
  CallToolResultSchema,
  type Tool as MCPToolDef,
  ToolListChangedNotificationSchema,
} from "@modelcontextprotocol/sdk/types.js"

这里的依赖可以分为两组:

  1. @modelcontextprotocol/sdk:MCP 官方 SDK,提供了 Client 类和三种传输实现(StdioClientTransportSSEClientTransportStreamableHTTPClientTransport)。这是与 MCP Server 通信的基础设施。

  2. ai(Vercel AI SDK):提供了 dynamicTool() 函数,用于将 MCP 工具转换为 AI SDK 的 Tool 格式。这是连接 MCP 世界与 LLM 世界的桥梁。

Instance.state() 状态管理

MCP 模块使用了我们在第 3 章介绍过的 Instance.state() 模式来管理状态:

回顾 Instance.state()

在第 3 章中我们学习过,Instance.state() 是 OpenCode 的"按项目目录隔离"状态管理模式。每个项目目录都有自己独立的一组 MCP 连接。当 OpenCode 启动时,state() 的初始化函数被调用,遍历配置中的所有 MCP Server 并尝试建立连接。当项目目录切换或 OpenCode 退出时,销毁函数被调用,优雅地关闭所有连接。

这里有一个重要的工程细节——所有 MCP Server 的连接是 并行建立 的(Promise.all),这确保了即使某个 MCP Server 启动较慢或超时,也不会阻塞其他 Server 的连接。

create() 函数:连接核心逻辑

create() 是整个 MCP 模块最核心的函数,它负责建立与单个 MCP Server 的连接。根据 type 字段的不同,连接流程分为两条路径:

远程 MCP Server(type: "remote"):

这段代码展示了一个精巧的 降级连接策略

  1. 首先尝试更现代的 StreamableHTTP 传输。

  2. 如果失败,自动回退到 SSE 传输。

  3. 如果遇到 UnauthorizedError(OAuth 认证错误),不立即失败,而是将连接标记为 needs_auth,等待用户完成认证后再重试。

本地 MCP Server(type: "local"):

本地连接相对简单——通过 StdioClientTransport 启动子进程,子进程的 stdin/stdout 作为通信通道。注意一个有趣的细节:当命令是 opencode 自身时(递归调用场景),会设置环境变量 BUN_BE_BUN: "1",这是 Bun 运行时的一个特殊标志。

连接建立后,无论是远程还是本地,都会立即获取工具列表来验证连接是否真正可用:

这是一个很好的防御性编程实践——不仅要连接成功,还要确保 Server 能正常响应请求。

8.2.2 MCP Server 连接管理

连接状态机

OpenCode 使用一个 Status 类型来跟踪每个 MCP Server 的连接状态。这个类型通过 Zod 的 discriminatedUnion 定义,确保了类型安全:

衍生解释:Discriminated Union(可辨识联合)

在 TypeScript 中,Discriminated Union 是一种常用的类型设计模式。联合类型中的每个变体都有一个共同的"判别字段"(在这里是 status),它的值是不同的字面量类型。这使得 TypeScript 编译器可以在 switchif 语句中自动收窄类型——例如当你检查 status === "failed" 时,TypeScript 会自动推断出 error 字段的存在。

Zod 的 z.discriminatedUnion() 是这一模式的运行时验证版本,它在解析 JSON 数据时也能利用判别字段来选择正确的验证分支。

五种状态的含义和转换关系:

值得注意的是,needs_authneeds_client_registration 这两个状态专门为 OAuth 认证场景设计。当远程 MCP Server 要求认证时,OpenCode 不会直接报错,而是将状态标记为"需要认证",并通过 Toast 通知用户:

ToolsChanged 事件监听

MCP 协议支持 Server 端主动通知 Client 端工具列表发生了变化。OpenCode 通过注册通知处理器来监听这一事件:

当收到 ToolListChangedNotification 时,OpenCode 通过 Bus(事件总线,详见第 11 章)发布一个 MCP.ToolsChanged 事件。这个事件会被上层模块(如 Session 系统)监听,触发工具列表的重新获取。

这意味着 MCP Server 可以在运行时动态添加或移除工具,而 OpenCode 能够实时感知这些变化——这是 MCP 协议比静态工具定义更加灵活的一个体现。

连接管理 API

MCP 模块暴露了一组连接管理函数,支持运行时的动态连接和断开:

connect() 函数在实现上有一个细节值得注意——它会强制将 enabled 设置为 true

add() 函数在添加新连接前会检查是否已有同名连接,如果有则先关闭旧连接以防止内存泄漏:

超时处理

MCP 连接和工具调用都使用了统一的超时机制:

withTimeout() 是 OpenCode 的一个工具函数(位于 util/timeout.ts),它包装一个 Promise,如果在指定时间内未完成则抛出超时错误。用户可以通过配置中的 timeout 字段为每个 MCP Server 单独设置超时时间,也可以使用全局的实验性配置 experimental.mcp_timeout

8.2.3 MCP 工具转换为内部 Tool 格式

MCP 系统中最关键的环节之一是将 MCP Server 提供的工具定义转换为 AI SDK 的 Tool 类型,使其能被 LLM 调用。这个转换由 convertMcpTool() 函数完成:

这个函数的核心逻辑可以分解为三步:

  1. Schema 标准化:MCP Server 提供的 inputSchema 可能不完全符合 JSON Schema 的规范(例如可能缺少 typeproperties 字段)。函数通过展开原始 Schema 并强制设置 type: "object"additionalProperties: false 来确保兼容性。

  2. 创建 dynamicTool():使用 AI SDK 的 dynamicTool() 工厂函数创建一个动态工具。dynamicTool() 与我们在第 5 章学习的 OpenCode 内置 Tool.define() 类似,但它来自 AI SDK,面向的是 LLM 的工具调用场景。

  3. 封装执行逻辑execute 函数通过 client.callTool() 将工具调用请求转发到 MCP Server。注意 resetTimeoutOnProgress: true 选项——这意味着如果 MCP Server 发送了进度通知,超时计时器会被重置,避免长时间运行的工具因超时而被错误中断。

工具名称的命名约定

在将 MCP 工具暴露给 LLM 时,OpenCode 使用了一个特定的命名约定来避免不同 MCP Server 之间的工具名称冲突:

例如,如果一个名为 my-jira 的 MCP Server 暴露了一个名为 search_issues 的工具,那么在 OpenCode 中这个工具的名称将是 my-jira_search_issues。这种前缀命名避免了当多个 MCP Server 暴露同名工具时的冲突问题。

Prompt 和 Resource 的获取

除了 Tool 之外,MCP 模块还提供了获取 Prompt 和 Resource 的函数:

Prompt 和 Resource 的命名也使用了类似的前缀策略:ServerName:PromptName / ServerName:ResourceName,使用冒号而非下划线分隔(与 Tool 的命名约定略有不同,因为它们不需要传递给 LLM 的函数调用接口)。


本节小结

OpenCode 的 MCP Client 实现建立在 @modelcontextprotocol/sdk 之上,通过 Instance.state() 管理连接生命周期。核心的 create() 函数根据配置类型(local/remote)选择相应的传输方式,远程连接采用 StreamableHTTP → SSE 的降级策略。连接状态通过五态状态机跟踪,特别为 OAuth 认证场景设计了 needs_authneeds_client_registration 两个中间状态。convertMcpTool() 函数将 MCP Tool 转换为 AI SDK 的 dynamicTool(),使 MCP 工具能够无缝融入 LLM 的工具调用体系。所有工具均使用"ServerName_ToolName"的前缀命名约定来避免冲突。

Last updated