目录

MCP 实现 — 协议支持与工具翻译

MCP 实现 — 协议支持与工具翻译

源码路径:/mnt/e/code/cc/opencode/packages/opencode/src/mcp/ 核心文件:mcp/index.ts 技术栈:@modelcontextprotocol/sdk + Effect + JSON Schema


1. 概述

OpenCode 对 MCP (Model Context Protocol) 的支持非常完整,不仅作为 MCP 客户端使用外部服务器,还实现了完整的 MCP 工具翻译层。相比 Claude Code,OpenCode 的 MCP 实现更加体系化。


2. MCP Service 接口

// packages/opencode/src/mcp/index.ts
export interface Interface {
  readonly status: () => Effect.Effect<Record<string, Status>>
  readonly clients: () => Effect.Effect<Record<string, MCPClient>>
  readonly tools: () => Effect.Effect<Record<string, Tool>>
  readonly prompts: () => Effect.Effect<Record<string, PromptInfo & { client: string }>>
  readonly resources: () => Effect.Effect<Record<string, ResourceInfo & { client: string }>>

  readonly add: (name: string, mcp: Config.Mcp) => Effect.Effect<{ status: Record<string, Status> | Status }>
  readonly connect: (name: string) => Effect.Effect<void>
  readonly disconnect: (name: string) => Effect.Effect<void>

  // OAuth 支持
  readonly auth: {
    startOAuth: (name: string) => Effect.Effect<void>
    handleCallback: (name: string, callbackUrl: string) => Effect.Effect<void>
  }
}

3. MCP 配置

// packages/opencode/src/config/config.ts
export namespace Mcp {
  // 本地 MCP 服务器
  export const Local = z.object({
    type: z.literal("local"),
    command: z.array(z.string()),
    args: z.array(z.string()).optional(),
    env: z.record(z.string()).optional(),
    enabled: z.boolean().optional(),
    timeout: z.number().optional(),
  })

  // 远程 MCP 服务器
  export const Remote = z.object({
    type: z.literal("remote"),
    url: z.string().url(),
    enabled: z.boolean().optional(),
    headers: z.record(z.string()).optional(),
    oauth: McpOAuth.optional(),
    timeout: z.number().optional(),
  })

  // OAuth 配置
  export const McpOAuth = z.object({
    clientId: z.string(),
    clientSecret: z.string(),
    scopes: z.array(z.string()).optional(),
    authUrl: z.string().optional(),
    tokenUrl: z.string().optional(),
  })
}

配置示例:

{
  "mcp": {
    "filesystem": {
      "type": "local",
      "command": ["npx", "@modelcontextprotocol/server-filesystem", "/path/to/dir"],
      "enabled": true
    },
    "github": {
      "type": "remote",
      "url": "https://mcp.github.com",
      "oauth": {
        "clientId": "xxx",
        "clientSecret": "{env:GITHUB_CLIENT_SECRET}",
        "scopes": ["repo", "user"]
      }
    }
  }
}

4. 传输层支持

// packages/opencode/src/mcp/index.ts

// 1. Stdio — 本地进程通信
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"
const transport = new StdioClientTransport({
  command: "npx",
  args: ["mcp-server", ...],
})

// 2. Streamable HTTP — HTTP 流式传输
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"
const transport = new StreamableHTTPClientTransport({
  url: "https://mcp.server.com/mcp",
  requestOptions: { headers: { Authorization: `Bearer ${token}` } },
})

// 3. SSE — Server-Sent Events
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js"
const transport = new SSEClientTransport(new URL("https://mcp.server.com/sse"))

5. 工具翻译层

5.1 MCP 工具转 OpenCode Tool

// packages/opencode/src/mcp/index.ts (lines 133-161)
function convertMcpTool(mcpTool: MCPToolDef, client: MCPClient, timeout?: number): Tool {
  const inputSchema = mcpTool.inputSchema

  // 转换为 JSON Schema
  const schema: JSONSchema7 = {
    ...(inputSchema as JSONSchema7),
    type: "object",
    properties: (inputSchema.properties ?? {}) as JSONSchema7["properties"],
    additionalProperties: false,
  }

  return dynamicTool({
    id: `${sanitizedClient}:${sanitizedToolName}`,
    description: mcpTool.description ?? "",
    inputSchema: jsonSchema(schema),

    execute: async (args: unknown) => {
      const result = await client.callTool({
        name: mcpTool.name,
        arguments: (args || {}) as Record<string, unknown>,
      }, CallToolResultSchema, {
        resetTimeoutOnProgress: true,
        timeout,
      })

      // 转换为 OpenCode 工具结果格式
      return {
        title: mcpTool.name,
        output: formatMcpResult(result),
        metadata: {
          client: sanitizedClient,
          toolName: mcpTool.name,
        },
      }
    },
  })
}

5.2 工具命名

// 工具名称格式: clientName:toolName
const sanitizedClient = clientName.replace(/[^a-zA-Z0-9_-]/g, "_")
const sanitizedToolName = mcpTool.name.replace(/[^a-zA-Z0-9_-]/g, "_")
const toolId = `${sanitizedClient}:${sanitizedToolName}`

6. OAuth 认证流程

// packages/opencode/src/mcp/index.ts
async function startOAuthFlow(config: McpOAuth, redirectUrl: string) {
  // 1. 动态客户端注册 (RFC 7591)
  const registration = await fetch(config.registrationUrl ?? DEFAULT_REG_URL, {
    method: "POST",
    body: JSON.stringify({
      client_name: "opencode",
      redirect_uris: [redirectUrl],
      grant_types: ["authorization_code"],
      response_types: ["code"],
    }),
  })

  // 2. 构建授权 URL
  const authUrl = new URL(config.authUrl ?? DEFAULT_AUTH_URL)
  authUrl.searchParams.set("client_id", registration.clientId)
  authUrl.searchParams.set("redirect_uri", redirectUrl)
  authUrl.searchParams.set("scope", config.scopes?.join(" ") ?? "read")
  authUrl.searchParams.set("response_type", "code")

  // 3. 打开浏览器授权
  await open(authUrl.toString())

  // 4. 等待回调
  const callbackUrl = await waitForCallback()

  // 5. 交换 token
  const token = await exchangeCodeForToken(callbackUrl, registration)
}

7. 状态管理

// MCP 服务器状态
export type Status =
  | "connected"    // 已连接
  | "disabled"     // 被禁用
  | "failed"       // 连接失败
  | "needs_auth"   // 需要 OAuth 认证
  | "needs_client_registration"  // 需要客户端注册

8. 与 Claude Code 对比

维度Claude CodeOpenCode
MCP 角色MCP 工具消费者MCP 工具消费者 + 完整 MCP 服务器
工具转换基础映射完整 JSON Schema 转换
传输协议stdiostdio + HTTP + SSE
OAuth完整 OAuth 流程支持
动态注册RFC 7591 客户端注册
提示词暴露MCP prompts → 命令
资源暴露MCP resources → 内部资源
超时控制工具级超时MCP 层超时 + resetTimeoutOnProgress

文档版本:v1.0 | 更新:2026-04-06