コンテンツにスキップ

MCP ソースコード解析

モジュール概要

MCP(Model Context Protocol)モジュールは、OpenCode の 外部ツール統合チャネル であり、packages/opencode/src/mcp/ に配置されています。MCP プロトコルを通じて、OpenCode はデータベースクライアント、ブラウザ自動化、検索エンジン、コード解析ツールなど、様々な外部ツールサーバーに接続でき、その機能を Agent が呼び出し可能なツールとして均一に公開します。MCP により、OpenCode の機能は組み込みのツールセットを超えて拡張できます。

モジュールのコアエントリポイント index.ts(約922行)は MCP 名前空間を公開し、Effect Service アーキテクチャパターンを採用しています。start()stop()client()status()tools() などの関数を公開します。内部では、Instance.state() 経由でグローバル状態を管理し、clients: Record<string, Client>status: Record<string, Status> を保持します。各 MCP サーバー接続は独立して管理され、1つのサーバーがクラッシュしても他のサーバーには影響しません。

コア設計の選択:

  • Effect Service パターンにより、タイプセーフな依存性注入とリソースライフサイクル管理を提供
  • 判別共用体型 により、5つのクライアント状態を正確に表現
  • convertMcpTool が MCP ツールを透過的に AI SDK dynamicTool に変換し、Agent は基盤プロトコルの違いを認識しない
  • OAuth 2.0 + PKCE 完全実装で、認証が必要なリモート MCP サーバーをサポート
  • 再帰的なプロセスクリーンアップ により、stdio モードで孤児プロセスが残らないことを保証

主要ファイル

ファイルパス行数責任範囲
src/mcp/index.ts約922モジュールのメインエントリ:MCP 名前空間、Effect Service パターン、クライアント作成、ツール発見、OAuth フロー、プロセスクリーンアップ
src/mcp/auth.ts約174McpAuth 名前空間、OAuth トークンの Zod スキーマ定義と永続化(mcp-auth.json、権限 0o600
src/mcp/oauth-callback.ts約217McpOAuthCallback 名前空間、ローカル HTTP コールバックサーバー(ポート 19876)、OAuth リダイレクトの処理と認証コードの抽出
src/mcp/oauth-provider.ts約186McpOAuthProvider クラス、OAuthClientProvider インターフェースを実装、OAuth クライアントのメタデータ、トークン、PKCE フローを管理

注意:MCP モジュールには個別の client.tstransport.ts ファイルはありません。トランスポート層の実装は @modelcontextprotocol/sdk npm パッケージから提供され、クライアントロジックは完全に index.ts に集中しています。この設計によりプロトコルの実装を SDK に委譲し、OpenCode は接続管理、ツール変換、認証オーケストレーションに専念します。

型システム

Status — クライアント状態の判別共用体

const Status = z.discriminatedUnion("status", [
  z.object({ status: z.literal("connected") }),
  z.object({ status: z.literal("disabled") }),
  z.object({ status: z.literal("failed"), error: z.string() }),
  z.object({ status: z.literal("needs_auth"), url: z.string() }),
  z.object({ status: z.literal("needs_client_registration") }),
])

5つの状態の意味と遷移パス:

状態意味ソース
connected正常接続、ツールが利用可能create() 成功、finishAuth() 成功
disabledユーザーが設定でこのサーバーを無効化したstart()mcp.disabled をチェック
failed接続失敗create() が例外をスロー、OAuth 失敗
needs_authOAuth 認証が必要、認証 URL を含むリモートサーバーが 401 を返した
needs_client_registration先に OAuth クライアントを登録する必要があるリモートサーバーが登録済みクライアントを検出できなかった

状態遷移チェーン:

disabled ──────────────────────────────────── (設定で制御、接続には関与しない)
needs_client_registration → needs_auth → connected
                                ↑               ↓
                             failed  ←──── (接続/OAuth 失敗)

McpAuth 型(auth.ts)

// OAuth トークン構造
const Tokens = z.object({
  access_token: z.string(),
  token_type: z.string(),
  scope: z.string().optional(),
  expires_at: z.number().optional(),   // Unix タイムスタンプ(秒)
  refresh_token: z.string().optional(),
})

// OAuth クライアント情報
const ClientInfo = z.object({
  client_id: z.string(),
  client_secret: z.string().optional(),
  client_secret_expires_at: z.number().optional(),
  redirect_uris: z.array(z.string()).optional(),
  grant_types: z.array(z.string()).optional(),
})

// 永続化エントリ:serverUrl でインデックス化
const Entry = z.object({
  serverUrl: z.string(),
  tokens: Tokens.optional(),
  clientInfo: ClientInfo.optional(),
  codeVerifier: z.string().optional(),
  state: z.string().optional(),
})

コアフロー

トランスポート層とクライアント作成

MCP.create(key, mcp) はクライアント作成のコア関数です。設定に基づいてトランスポート方式を選択します:

// ローカルプロセス:stdio トランスポート
if (mcp.type === "stdio") {
  transport = new StdioClientTransport({
    command: mcp.command,
    args: mcp.args,
    env: { ...process.env, ...mcp.env },
    stderr: "pipe",
  })
}

// リモートサービス:StreamableHTTP を優先、SSE にフォールバック
if (mcp.url) {
  const url = new URL(mcp.url)
  transport = new StreamableHTTPClientTransport(url, {
    // この URL に OAuth 認証情報が保存されている場合、OAuthProvider を注入
    authProvider: hasStoredCredentials ? oAuthProvider : undefined,
  })
}

3つのトランスポート方式の技術的特徴:

トランスポート方式クラス名通信方式ユースケース
stdioStdioClientTransport標準入出力パイプローカル MCP サーバープロセス(最も一般的)
StreamableHTTPStreamableHTTPClientTransportHTTP POST + ストリーミング応答新しいリモート MCP サーバー
SSESSEClientTransportHTTP Server-Sent Eventsレガシーリモートサーバー互換性

リモートトランスポートは自動フォールバックをサポートしています:最初に StreamableHTTPClientTransport を試み、サーバーがサポートしていなければ SSEClientTransport にフォールバックします。リモートトランスポートは透過的な認証のために McpOAuthProvider を注入することもできます。

Effect リソース管理 — acquireUseRelease

create() 関数は Effect の acquireUseRelease パターンを使用してトランスポート接続のライフサイクルを管理します:

yield* Effect.acquireUseRelease(
  // acquire: 接続を確立
  Effect.tryPromise(() => client.connect(transport)),

  // use: 正常接続後の操作(ツールリストの取得など)
  async (connected) => {
    // ... ツール発見、状態の更新 ...
  },

  // release: 成功失敗に関係なくトランスポートが閉じられることを保証
  (connected, exit) => {
    if (exit._tag === "Failure") {
      // 接続失敗 → トランスポートを閉じてリソースを解放
      return Effect.tryPromise(() => transport.close())
    }
  },
)

このパターンにより、接続中にエラーが発生してもトランスポートが適切に閉じられ、ファイルディスクリプタや子プロセスのリークを防ぎます。

全 MCP サーバーの並列初期化

start() 関数は Effect の forEach を使用して、設定された全 MCP サーバーを並列に初期化します:

// 並列初期化、コンカレンシー制限なし
yield* Effect.forEach(
  Object.entries(config.mcp),
  ([key, mcp]) => create(key, mcp),
  { concurrency: "unbounded" },
)

concurrency: "unbounded" は全サーバーが同時に起動することを意味します。1つの遅いサーバーが他をブロックしません。接続に失敗したサーバーは failed とマークされますが、他のサーバーの初期化には影響しません。

ツール発見と登録

MCP サーバー接続成功後のツール発見フロー:

  1. defs() 関数が client.listTools() を呼び出して、サーバーの宣言済みツールリストを取得します
  2. タイムアウト保護を設定して、応答のないサーバーが全体起動フローをブロックするのを防ぎます
  3. convertMcpTool(mcpTool, client, timeout) が各 MCP ツールを AI SDK dynamicTool に変換します
  4. ツールの名前付け規則:sanitizedClientName + "_" + sanitizedToolName(例:github_search_repositories
  5. tools/list_changed 通知ハンドラを登録して、サーバーが動的にツールを追加または削除したときにツールが自動的に再発見されるようにします

convertMcpTool — MCP ツールから AI SDK ツールへのブリッジ

これは MCP モジュールの最も重要な変換関数であり、MCP プロトコルのツール定義を AI SDK dynamicTool に変換します:

function convertMcpTool(
  mcpTool: MCPToolDef,
  client: MCPClient,
  timeout?: number,
): Tool {
  // 1. パラメータスキーマ変換:MCP の inputSchema を直接パススルー
  const inputSchema = mcpTool.inputSchema
  const schema: JSONSchema7 = {
    ...(inputSchema as JSONSchema7),
    type: "object",
    properties: (inputSchema.properties ?? {}) as JSONSchema7["properties"],
    // 厳格なバリデーション:追加プロパティを許可しない
    additionalProperties: false,
  }

  // 2. dynamicTool を構築
  return dynamicTool({
    description: mcpTool.description ?? "",
    inputSchema: jsonSchema(schema),
    // 3. エクゼキュータ:MCP クライアントの callTool を呼び出す
    execute: async (args: unknown) => {
      return client.callTool(
        {
          name: mcpTool.name,
          arguments: (args || {}) as Record<string, unknown>,
        },
        CallToolResultSchema,
        {
          resetTimeoutOnProgress: true,  // 進捗更新時にタイムアウトを自動リセット
          timeout,
        },
      )
    },
  })
}

主な設計上の決定:

  • パラメータスキーマのパススルー:MCP ツールの inputSchema が AI SDK ツールのパラメータ定義として直接使用され、手動マッピングが不要
  • additionalProperties: false:排他的厳格バリデーション — LLM が生成したパラメータはスキーマと完全に一致する必要があり、予期しないフィールドが MCP サーバーに渡されるのを防止
  • resetTimeoutOnProgress:長時間実行されるツール(例:大規模検索)は進捗更新を受け取る際にタイムアウトを自動リセットし、誤って終了させられることを回避
  • 名前付け規則sanitizedClientName + "_" + sanitizedToolName により、異なる MCP サーバーのツール名が競合しないことを保証

tools/list_changed 通知の処理

MCP プロトコルはサーバーが実行時に動的にツールを追加または削除することをサポートしています。watch() 関数は tools/list_changed 通知をリッスンします:

client.setNotificationHandler(ToolsListChangedNotificationSchema, async () => {
  // 1. ツールリストを再取得
  const newTools = await defs(client, key, mcp)
  // 2. 状態内のツールセットを更新
  // 3. Bus を介して ToolsChanged イベントをブロードキャスト
  Bus.publish(Event.ToolsChanged, { client: key })
})

これにより、Agent は MCP サーバーが動的に追加したツールに、再起動なしでアクセスできます。

OAuth 2.0 + PKCE 認証フロー

リモート MCP サーバーには OAuth 認証が必要な場合があります。OpenCode は3つのファイルにまたがる完全な OAuth 2.0 + PKCE フローを実装しています:

完全なフロー

1. startAuth(key, mcp)
   ├─ ランダムな state パラメータを生成(CSRF 保護)
   ├─ McpOAuthProvider インスタンスを作成
   ├─ リモートサーバーへの接続を試行
   └─ authorizationUrl をキャプチャ(Provider リダイレクトから抽出)

2. authenticate(key, mcp)
   ├─ 認証 URL を取得するために startAuth() を呼び出す
   ├─ ユーザーブラウザを開く(Open.browser)
   └─ waitForCallback(oauthState, mcpName) ← ブロッキング待機、5分のタイムアウト

3. ブラウザコールバック → McpOAuthCallback が処理
   ├─ ユーザーが承認した後、http://localhost:19876/mcp/oauth/callback にリダイレクト
   ├─ state パラメータを検証(CSRF 保護)
   └─ 認証コードを解析

4. finishAuth(key, mcp, code)
   ├─ transport.finishAuth(code) でアクセストークンを交換
   └─ createAndStore() で認証情報を永続化

5. McpAuth.set() → mcp-auth.json に書き込み(権限 0o600)

McpOAuthCallback — ローカルコールバックサーバー

oauth-callback.ts は OAuth コールバックを受け取る軽量なローカル HTTP サーバーを実装しています:

// 定数定義
const OAUTH_CALLBACK_PORT = 19876           // 固定コールバックポート
const OAUTH_CALLBACK_PATH = "/mcp/oauth/callback"  // コールバックパス
const CALLBACK_TIMEOUT_MS = 5 * 60 * 1000  // 5分のタイムアウト

コアデータ構造:

// 保留中の認証マッピング:state → Promise resolve 関数
const pendingAuths: Map<string, (code: string) => void> = new Map()

// 逆インデックス:mcpName → oauthState(特定の MCP の認証をキャンセルするため)
const mcpNameToState: Map<string, string> = new Map()

ensureRunning() メソッドはポートがすでに使用されているかどうかをチェックします:

  • ポートが空の場合、ポート 19876 でリッスンする HTTP サーバーを起動します
  • ポートがすでに使用されている場合(別の OpenCode インスタンスが実行中の意味)、既存のサーバーを再利用します

handleRequest() メソッドはコールバックリクエストを処理します:

  1. URL から statecode パラメータを抽出します
  2. statependingAuths に存在することを検証します(CSRF 保護)
  3. 対応する resolve(code) を呼び出して、待機中の authenticate() コールを起動します
  4. カスタム HTML ページを返します:成功ページ(緑)またはエラーページ(赤)

McpOAuthProvider — OAuth クライアントプロバイダー

oauth-provider.ts@modelcontextprotocol/sdk が要求する標準インターフェースである OAuthClientProvider インターフェースを実装しています:

class McpOAuthProvider implements OAuthClientProvider {
  // クライアントメタデータ:認証サーバーに自分が誰かを伝える
  get clientMetadata(): OAuthClientMetadata {
    return {
      redirect_uris: [`http://localhost:${OAUTH_CALLBACK_PORT}${OAUTH_CALLBACK_PATH}`],
      client_name: "OpenCode",
      grant_types: ["authorization_code", "refresh_token"],
    }
  }

  // クライアント情報:優先度でルックアップ
  // 1. ユーザー設定からの client_id/client_secret
  // 2. McpAuth から保存された clientInfo
  // 3. なし → ダイナミッククライアント登録(DCR)をトリガー
  get clientInformation(): OAuthClientInformation | undefined { ... }

  // トークン:McpAuth から取得して形式変換
  get tokens(): OAuthTokens | undefined { ... }

  // 認証ページへのリダイレクト:実際にナビゲートする代わりに URL をキャプチャ
  async redirectToAuthorization(authorizationUrl: URL): Promise<void> {
    this.capturedAuthorizationUrl = authorizationUrl
  }

  // PKCE コードベリファイアストレージ
  async saveCodeVerifier(codeVerifier: string): Promise<void> { ... }
  get codeVerifier(): string | undefined { ... }

  // CSRF state パラメータストレージ
  async saveState(state: string): Promise<void> { ... }
  get state(): string | undefined { ... }

  // 無効化時に保存された認証情報をクリア
  async invalidateCredentials(kind: "client" | "tokens" | "all"): Promise<void> { ... }
}

invalidateCredentials は3つの無効化シナリオを処理します:

  • "client":クライアント情報をクリア(例:clientSecret が期限切れ、clientSecretExpiresAt をチェック)
  • "tokens":トークンをクリア(例:refresh_token なしで access_token が期限切れ)
  • "all":すべての認証情報をクリア、認証フローを再開

McpAuth — トークンの永続化

auth.ts は Effect Service パターンを使用して OAuth 認証情報を安全に保存します:

// ファイルパス:Global.Path.data/mcp-auth.json
// ファイル権限:0o600(所有者のみ読み書き可)

// コアヘルパー関数
async function writeJson(data: Entry[]): Promise<void> {
  await FileSystem.writeFile(
    Global.Path.data + "/mcp-auth.json",
    JSON.stringify(data, null, 2),
    { mode: 0o600 },  // 厳格なファイル権限
  )
}

主なセキュリティ対策:

  • ファイル権限 0o600:他のユーザーが OAuth トークンを読み取れないことを保証
  • URL マッチング検証getForUrl(serverUrl) は認証情報を返す際に serverUrl が一致することを検証し、認証情報のクロス使用を防止
  • トークン期限切れチェックisTokenExpired()expiresAt フィールドをチェック;期限切れのトークンは自動的には使用されません

汎用ヘルパー関数 updateFieldclearField はタイプセーフなフィールドレベルの更新を提供します:

// URL エントリの特定のフィールドを更新
async function updateField<T extends keyof Entry>(
  serverUrl: string,
  field: T,
  value: Entry[T],
): Promise<void> { ... }

// URL エントリの特定のフィールドをクリア
async function clearField(
  serverUrl: string,
  field: keyof Entry,
): Promise<void> { ... }

プロセスクリーンアップ

MCP モジュールは stdio トランスポートモードで特別なプロセスクリーンアップ要件があります。ローカル MCP サーバーは通常子プロセスとして起動され、これらの子プロセス自体が子プロセスを作成することがあります(例:GitHub MCP サーバーが git サブプロセスを起動する可能性があります)。OpenCode は再帰的なプロセスクリーンアップを実装して、孤児プロセスが残らないことを保証します。

descendants — プロセスツリーの再帰的取得

// BFS(幅優先探索)を使用してプロセスのすべての子孫を再帰的に取得
const pids: number[] = []
const queue = [pid]  // MCP サーバープロセス PID から開始

while (queue.length > 0) {
  const current = queue.shift()!
  // pgrep -P で現在のプロセスの直接の子を取得
  const handle = yield* spawner.spawn(
    ChildProcess.make("pgrep", ["-P", String(current)], ...),
  )
  const text = yield* Stream.mkString(Stream.decodeText(handle.stdout))
  for (const tok of text.split("\n")) {
    const cpid = parseInt(tok, 10)
    if (!isNaN(cpid)) {
      pids.push(cpid)
      queue.push(cpid)  // 子プロセスの子の検索を継続
    }
  }
}

クリーンアップ戦略

MCP クライアントを閉じるときの完全なクリーンアップフロー:

1. client.close() で MCP クライアント接続を閉じる
2. stdio トランスポートがあるかどうかをチェック(pid プロパティがある場合)
   ├─ pid なし(リモートトランスポート)→ クリーンアップ完了
   └─ pid あり(stdio トランスポート)→ 続行
3. descendants(pid) ですべての子孫プロセスを取得
4. リーフプロセス(リストの末尾)から始めて SIGTERM を送信
5. 5秒間待機
6. まだ生きている?SIGKILL で強制終了
7. pendingOAuthTransports Map をクリア

リーフプロセスから SIGTERM を送信する理由:親プロセスを先にkillすると、子プロセスが init プロセスに再配置されて孤児になる可能性があります。リーフプロセスを先にkillすることで、プロセスツリーを下から上に向かって完全にクリーンアップできます。

Instance Dispose 時の自動クリーンアップ

インスタンス破棄時のクリーンアップロジックは Effect.addFinalizer で登録されます:

Effect.addFinalizer(() =>
  Effect.gen(function* () {
    // すべてのクライアントをイテレート、上記のクリーンアップフローを実行
    for (const [key, client] of Object.entries(state.clients)) {
      // ... close + descendants + kill ...
    }
    // pendingOAuthTransports をクリア
    pendingOAuthTransports.clear()
  }),
)

これにより、インスタンスが正常にシャットダウンした場合も異常終了した場合も、子プロセスが適切にクリーンアップされます。

呼び出しチェーンの例

チェーン1:設定読み込み → MCP 起動 → ツール登録

1. Instance.init() がトリガー → MCP.start() が呼び出される


2. Config.mcp を読み込んでサーバー一覧を取得
   例:{ "github": { type: "stdio", command: "npx", args: ["-y", "@modelcontextprotocol/server-github"] } }


3. Effect.forEach({ concurrency: "unbounded" }) で並列初期化
   各設定に対して create(key, mcp) を呼び出す:
   ├─ mcp.disabled をチェック → disabled 状態
   ├─ トランスポート層を選択:
   │  ├─ stdio → StdioClientTransport → 子プロセスを起動
   │  └─ url → StreamableHTTPClientTransport(SSE フォールバック)
   ├─ Effect.acquireUseRelease で接続を確立
   │  ├─ acquire: client.connect(transport)
   │  ├─ use: client.listTools() → convertMcpTool() で変換
   │  └─ release: 失敗時にトランスポートを閉じる
   └─ status[key] 状態を更新


4. defs(client, key, mcp) でツール発見
   ├─ client.listTools() でツールリストを取得(タイムアウト保護付き)
   └─ convertMcpTool() で各ツールを dynamicTool に変換
      例:{ name: "search_repositories" } → dynamicTool "github_search_repositories"


5. Bus.publish(ToolsChanged) で Agent にツールリスト更新を通知


6. Agent の次のプロンプト構築時に MCP ツールの説明が含まれる

チェーン2:Agent が MCP ツールを呼び出す

1. LLM がツール名 "github_search_repositories" で tool_use を返す


2. Agent ツールルーティングがこの MCP ツールにマッチ
   → dynamicTool.execute({ query: "opencode" })


3. convertMcpTool の内部で:
   client.callTool({
     name: "search_repositories",          // 元の MCP ツール名
     arguments: { query: "opencode" },     // LLM が生成したパラメータ
   }, CallToolResultSchema, {
     resetTimeoutOnProgress: true,         // 進捗更新時にタイムアウトをリセット
     timeout: 30000,                       // デフォルト30秒のタイムアウト
   })


4. MCP SDK がトランスポート層経由で MCP サーバーにリクエストを送信
   ├─ stdio: 子プロセスの stdin に書き込み
   └─ HTTP: リモートサーバーに POST リクエスト


5. MCP サーバーがリクエストを処理して結果を返す


6. 結果が Agent ツールの戻り値を通じて LLM に渡される

チェーン3:完全な OAuth 認証フロー

1. ユーザーが認証が必要なリモート MCP サーバーを起動
   MCP.create("remote-server", { url: "https://..." })
   ├─ StreamableHTTPClientTransport 接続を試行
   └─ サーバーが 401 Unauthorized を返す


2. 状態が needs_auth に更新され、authorizationUrl を含む


3. MCP.authenticate("remote-server", mcp)
   ├─ startAuth():
   │  ├─ ランダムな state を生成(CSRF 保護)
   │  ├─ McpOAuthProvider を作成(PKCE コードベリファイア付き)
   │  ├─ 接続を試行 → authorizationUrl をキャプチャ
   │  └─ { authorizationUrl, oauthState } を返す

   ├─ Open.browser(authorizationUrl) → ユーザーのブラウザを開く

   └─ McpOAuthCallback.waitForCallback(oauthState, "remote-server")
      ├─ ensureRunning() → ポート 19876 で HTTP サーバーが実行されていることを確認
      ├─ Promise を pendingAuths[state] に保存
      └─ 最大5分間 resolve を待機


4. ユーザーがブラウザでログインして承認
   → http://localhost:19876/mcp/oauth/callback?state=xxx&code=yyy にリダイレクト


5. McpOAuthCallback.handleRequest()
   ├─ state パラメータが一致することを検証(CSRF 保護)
   ├─ code を解析(認証コード)
   ├─ resolve(code) → 待機中の authenticate() を起動
   └─ 成功 HTML ページを返す


6. finishAuth("remote-server", mcp, code)
   ├─ transport.finishAuth(code) → 認証コードを access_token と交換
   ├─ McpAuth.set() → mcp-auth.json に永続化(権限 0o600)
   └─ createAndStore() → 新しい認証情報で再接続


7. 状態が connected に更新され、ツール発見が開始

チェーン4:サーバーのシャットダウンとリソースクリーンアップ

1. Instance.shutdown() → MCP.stop() が呼び出される


2. すべてのクライアントをイテレート
   ├─ client.close() で接続を閉じる
   └─ stdio トランスポートのプロセスクリーンアップを実行:

      ├─ descendants(pid) でプロセスツリーを再帰的に取得
      │  例:pid=1000 → [1001, 1002, 1003]

      ├─ リーフプロセス(リストの末尾)から SIGTERM を送信
      │  kill(1003, SIGTERM) → kill(1002, SIGTERM) → kill(1001, SIGTERM)

      ├─ 5秒間待機

      ├─ まだ生きている?→ SIGKILL で強制終了

      └─ クリーンアップ完了


3. pendingOAuthTransports Map をクリア
   進行中のすべての OAuth フローを終了

設計上のトレードオフ

決定根拠
Effect Service パターンタイプセーフな依存性注入とリソースライフサイクル管理を提供します。acquireUseRelease はトランスポート接続がすべてのケースで適切に閉じられることを保証し、addFinalizer は子プロセスが再帰的にクリーンアップされることを保証します
MCP SDK トランスポート層への委譲トランスポートプロトコルの複雑な詳細(stdio パイプ管理、HTTP SSE パーシング、プロトコルバージョン交渉)は @modelcontextprotocol/sdk が処理し、OpenCode は接続オーケストレーションとツール変換に専念します
additionalProperties: false排他的厳格バリデーション。MCP ツールのパラメータスキーマは不完全な場合がありますが、AI SDK は LLM が生成したパラメータが厳密に一致することを要求します。予期しないフィールドを MCP サーバーに渡すよりも、LLM に再試行させる方がましです
パラメータスキーマの直接パススルーMCP スキーマ → AI SDK スキーマのマッピング変換なし。MCP の inputSchema はすでに JSON Schema であり、AI SDK の jsonSchema() と互換性があります。変換層が少ないほどバグも少なくなります
StreamableHTTP → SSE 自動フォールバックMCP プロトコルは SSE から StreamableHTTP への移行中ですが、多くのサーバーはまだ SSE のみをサポートしています。自動フォールバックにより両方のタイプのサーバーに接続できます
固定コールバックポート 19876OAuth コールバックには redirect_uri に事前登録するために予測可能なポートが必要です。ensureRunning() がポートの競合を処理し、複数の OpenCode インスタンスが同じコールバックサーバーを共有できます
再帰的プロセスクリーンアップ(BFS + リーフ優先)stdio MCP サーバーは子プロセスのチェーンを起動する可能性があります。親を先にkillすると子が init に再配置されて孤児になります。BFS トラバーサル + リーフからの SIGTERM で完全なプロセスツリーのクリーンアップを保証します
5分の OAuth タイムアウトOAuth はユーザーがブラウザでマニュアル操作する必要があります。5分はログインと承認を完了するのに十分な時間を与え、無限のハングを防ぎます
ファイル権限 0o600OAuth トークンはパスワードと同等です。0o600(所有者のみ読み書き可)はマルチユーザーシステムでの基本的なセキュリティ対策です
concurrency: "unbounded" 並列初期化MCP サーバーは完全に独立しています。1つが遅くても他に影響しません。コンカレンシー制限なしで全サーバーができるだけ早く準備完了できます

他のモジュールとの関係

  • Agent:Agent は統合ツールインターフェース経由で MCP ツールを呼び出し、組み込みツールと区別できません。convertMcpTool() が MCP ツールを AI SDK dynamicTool としてラップし、ツールの説明は自動的に Agent コンテキストに注入されます。Agent の resolveTools() は組み込みツールと MCP ツールを同時に登録します
  • ConfigConfig.mcp が MCP サーバー一覧を定義し、各アイテムは type(stdio/url)、commandargsenvurldisabled などのフィールドを含みます。設定変更後、再度 start() を呼び出す必要があります
  • Instance:MCP 状態は Instance.state() 経由で管理され、プロジェクトインスタンスのライフサイクルに従います。start()/stop() はインスタンスの初期化/シャットダウン時に自動的に呼び出されます。Effect.addFinalizer はインスタンスが破棄されたときに子プロセスがクリーンアップされることを保証します
  • BusToolsChanged BusEvent(BusEvent.define("mcp.tools.changed"))がツールリストの変更を通知し、Agent と UI 層が更新をサブスクライブします。BrowserOpenFailed イベントが OAuth ブラウザ起動失敗時のデグラデーションを処理します
  • GlobalGlobal.Path.data/mcp-auth.json が OAuth トークンを保存します。ファイル権限 0o600 がセキュリティを保証します。パスは Global モジュールで統一管理されます
  • Permission:MCP ツールの呼び出しも Permission システムの管理下にあります。ユーザーは特定の MCP ツールの実行を承認または拒否でき、組み込みツールと同じ権限ルールを使用します
  • Provider:MCP ツールの実行結果は dynamicTool 経由で Agent に返され、Provider モジュールの LLM コールフローにシームレスに統合されます