# MCP 集成

> 本地 stdio 与远程 HTTP / SSE 服务器的连接与五种状态、工具与资源如何交给模型、带 CSRF 校验的 OAuth 流程，以及 opencode mcp 命令。

- 作者：David（道雾轩）
- 专栏：OpenCode 源码解读（https://daiw.org/manual/opencode.md）
- 最后更新：2026-09-19
- 原文：https://daiw.org/manual/opencode/mcp
- 转载与引用：请注明出处并附原文链接（https://daiw.org/about/copyright）

MCP（Model Context Protocol）的实现集中在 `packages/opencode/src/mcp/`：`index.ts` 管连接与状态，`catalog.ts` 把工具、提示、资源翻译成 OpenCode 的形状，`auth.ts`、`oauth-provider.ts`、`oauth-callback.ts` 负责远程服务器的 OAuth，`browser.ts` 只做一件事——打开浏览器。

## 配置写法

字段定义在 `packages/core/src/v1/config/mcp.ts`，以 `type` 区分两类：

```jsonc
{
  "mcp": {
    "local-tools": {
      "type": "local",
      "command": ["bunx", "my-mcp-server", "--stdio"],
      "environment": { "API_TOKEN": "{env:API_TOKEN}" },
      "timeout": 10000
    },
    "docs": {
      "type": "remote",
      "url": "https://docs.example.com/mcp",
      "headers": { "X-Team": "infra" },
      "oauth": { "scope": "read" }
    },
    // 只写 enabled：开关一个在别处（例如组织下发的远端配置）定义好的服务器
    "jira": { "enabled": true }
  }
}
```

- `command` 是数组，首项为可执行文件；`cwd` 相对实例目录解析；子进程环境是 `process.env` 叠加 `environment`。
- 远程服务器默认挂上 OAuth provider 做自动探测，`oauth: false` 才彻底关闭；`oauth` 对象可给 `clientId`、`clientSecret`、`scope`、`callbackPort`、`redirectUri`。
- 只含 `enabled` 的条目是 schema 专门留的口子，靠深合并补全；合并后仍没有 `type` 的条目，运行时记一条错误并忽略。

<Callout type="info">
  `timeout` 的 schema 描述写着“默认 5 秒”，但代码里连接与列工具的兜底都是 `DEFAULT_TIMEOUT = 30_000`；工具调用依次取服务器的 `timeout`、`experimental.mcp_timeout`，都没有时交给 SDK 的默认值。
</Callout>

## 连接：本地 stdio，远程先 HTTP 后 SSE

`MCP.state` 在实例内首次被访问时（列命令、组装工具或查询状态）建立，届时并发连接所有启用的服务器。本地服务器用 `StdioClientTransport` 起子进程；远程服务器先试 `StreamableHTTPClientTransport`，失败再退回 `SSEClientTransport`——但如果失败原因是鉴权，就不再尝试下一种，而是把状态置为 `needs_auth`（弹出提示运行 `opencode mcp auth <name>`），或在服务器不支持动态注册时置为 `needs_client_registration`。

客户端声明的能力只有一项：

```ts
const CLIENT_OPTIONS = {
  capabilities: {
    // https://github.com/anomalyco/opencode/issues/11948
    // sampling: {},
    // https://github.com/anomalyco/opencode/issues/23066
    // elicitation: {},
    // https://github.com/anomalyco/opencode/issues/2308
    roots: {},
    // https://github.com/anomalyco/opencode/issues/28567
    // tasks: {},
  },
} satisfies ClientOptions
```

（`packages/opencode/src/mcp/index.ts:39-50`）

sampling、elicitation、tasks 都被注释掉，每行挂着对应的 issue；唯一开启的 `roots` 用实例目录作答。

## 五种状态

| 状态 | 含义 |
| --- | --- |
| `connected` | 已连接，工具列表已缓存 |
| `disabled` | `enabled: false`，或被手动断开 |
| `failed` | 连接或列工具出错，附错误信息 |
| `needs_auth` | 需要 OAuth 授权 |
| `needs_client_registration` | 需要预先注册的 client ID |

连接建立后还挂着三个监听：连接断开时标为 `failed` 并广播 `mcp.tools.changed`；收到 `notifications/tools/list_changed` 通知时重新拉取工具定义；服务器日志按级别转成 OpenCode 自己的日志。实例销毁时，对 stdio 服务器先用 `pgrep -P` 递归找出子孙进程逐个 `SIGTERM`（Windows 上跳过），再关闭连接。

## 交给模型：工具、资源、提示与说明

**工具**名是「服务器名_工具名」，两段都把 `[a-zA-Z0-9_-]` 以外的字符换成下划线：

```ts
export const sanitize = (value: string) => value.replace(/[^a-zA-Z0-9_-]/g, "_")

export const toolName = (clientName: string, name: string) => sanitize(clientName) + "_" + sanitize(name)
```

（`packages/opencode/src/mcp/catalog.ts:117-119`）

对照 [Kimi Code 的做法](https://daiw.org/manual/kimi-code/mcp)：那边用 `mcp__` 前缀、双下划线分隔，并对超过 64 字符的名字做哈希截断；这里没有前缀，`catalog.ts` 里也没有长度处理。

`convertTool()` 把 MCP 工具包成 AI SDK 的 `dynamicTool`：输入 schema 强制为 `type: "object"` 且禁止额外属性；调用时开启 `resetTimeoutOnProgress`，注释解释了为什么要塞一个空的 `onprogress`——“The MCP SDK only sends a progress token when this hook is present, enabling timeout resets.”；结果带 `isError` 就抛错；只有 `structuredContent` 时序列化成文本。列工具若因 `outputSchema` 的引用解析失败，会换一个忽略它的宽松 schema 重试。

**权限**：`session/tools.ts` 在每次调用前以工具名本身作为权限键询问，所以能用通配符管住整个服务器：

```jsonc
{ "permission": { "github_*": "deny", "docs_search": "allow" } }
```

pattern 为 `*` 的 `deny` 规则还会让这些工具从请求里整个消失（`Permission.disabled`），模型根本看不见。

**资源**：只要有服务器声明了 resources 能力，就额外注册 `list_mcp_resources`、`list_mcp_resource_templates`、`read_mcp_resource` 三个合成工具，走 `read` 权限，pattern 形如 `mcp:<server>:<uri>`。用户也能在输入框里直接引用资源；二进制内容只收白名单 MIME，上限 10 MB。

**提示**变成斜杠命令（见[上一篇](https://daiw.org/manual/opencode/agents-and-commands)）。**服务器说明**——`initialize` 返回的 `instructions`——被包进 `<mcp_instructions>` 放入系统提示词；某服务器的工具对当前 agent 全部被禁时，它的说明也一并省略。开启 [Code Mode](https://daiw.org/manual/opencode/web-and-codemode) 后，MCP 工具不再逐个注册，改由 `execute` 工具统一调度。

## OAuth：state、回调与延迟提交

```mermaid
sequenceDiagram
  participant U as opencode mcp auth
  participant M as MCP.authenticate
  participant C as 本地回调服务器
  participant A as 授权服务器
  U->>M: authenticate(name)
  M->>C: ensureRunning()
  M->>M: 生成随机 state 并落盘
  M->>A: 连接被拒，拿到授权 URL
  M->>U: 打开浏览器
  A->>C: 回调携带 code 与 state
  C->>M: 按 state 找到挂起的请求
  M->>M: 与落盘的 state 再核对一次
  M->>A: finishAuth(code) 换取 token
  M->>M: commit() 写入凭据并重连
```

- 回调地址默认 `http://127.0.0.1:19876/mcp/oauth/callback`，可用 `callbackPort` 或 `redirectUri` 修改；端口已被占用时不再另起服务器，等待回调最长 5 分钟。
- 回调缺少 `state`，或 `state` 不在挂起表中，一律按 CSRF 拒绝。
- 没配 `clientId` 时走动态客户端注册。注册结果与 token 存进数据目录的 `mcp-auth.json`（权限 `0o600`，读写加文件锁），并**绑定服务器 URL**：URL 一变，旧凭据自动失效。
- 授权期间用的是 `McpOAuthPendingProvider`，token 与注册信息先放内存，成功后才 `commit()` 落盘，半途失败不会留下脏凭据。

## `opencode mcp` 命令

| 子命令 | 作用 |
| --- | --- |
| `mcp add [name]` | 添加服务器，用 `jsonc-parser` 写入以保留注释 |
| `mcp list` / `mcp ls` | 列出服务器与连接状态 |
| `mcp auth [name]` | 发起 OAuth；`mcp auth list` 查看授权状态 |
| `mcp logout [name]` | 删除已存凭据 |
| `mcp debug <name>` | 手工发一次 `initialize`，诊断 401、`WWW-Authenticate` 与动态注册 |

非交互添加（写入全局配置）：

```bash
opencode mcp add docs --url https://docs.example.com/mcp --header "X-Team=infra"
opencode mcp add local-tools --env API_TOKEN=xxx -- bunx my-mcp-server --stdio
```

交互模式下，若当前目录是 git 仓库，会先问写进项目还是全局。服务端同样暴露了状态查询、运行时添加、连接与断开以及整套授权接口（`server/routes/instance/httpapi/handlers/mcp.ts`）。

## V2 的形状

V2 配置（`packages/core/src/config/mcp.ts`）把服务器收进 `mcp.servers`，用 `disabled` 取代 `enabled`，OAuth 字段改为蛇形命名，超时拆成 `startup`（建立连接与初始化）和 `request`（每个请求）两个预算：可以在 `mcp.timeout` 设全局默认，再在单个服务器上覆盖其一。

下一篇：[LSP 与格式化器](https://daiw.org/manual/opencode/lsp-and-format)——38 个内置语言服务器如何按需启动，诊断又怎样回到模型手里。
