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 SDKdynamicToolに変換し、Agent は基盤プロトコルの違いを認識しない- OAuth 2.0 + PKCE 完全実装で、認証が必要なリモート MCP サーバーをサポート
- 再帰的なプロセスクリーンアップ により、stdio モードで孤児プロセスが残らないことを保証
主要ファイル
| ファイルパス | 行数 | 責任範囲 |
|---|---|---|
src/mcp/index.ts | 約922 | モジュールのメインエントリ:MCP 名前空間、Effect Service パターン、クライアント作成、ツール発見、OAuth フロー、プロセスクリーンアップ |
src/mcp/auth.ts | 約174 | McpAuth 名前空間、OAuth トークンの Zod スキーマ定義と永続化(mcp-auth.json、権限 0o600) |
src/mcp/oauth-callback.ts | 約217 | McpOAuthCallback 名前空間、ローカル HTTP コールバックサーバー(ポート 19876)、OAuth リダイレクトの処理と認証コードの抽出 |
src/mcp/oauth-provider.ts | 約186 | McpOAuthProvider クラス、OAuthClientProvider インターフェースを実装、OAuth クライアントのメタデータ、トークン、PKCE フローを管理 |
注意:MCP モジュールには個別の client.ts や transport.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_auth | OAuth 認証が必要、認証 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つのトランスポート方式の技術的特徴:
| トランスポート方式 | クラス名 | 通信方式 | ユースケース |
|---|---|---|---|
| stdio | StdioClientTransport | 標準入出力パイプ | ローカル MCP サーバープロセス(最も一般的) |
| StreamableHTTP | StreamableHTTPClientTransport | HTTP POST + ストリーミング応答 | 新しいリモート MCP サーバー |
| SSE | SSEClientTransport | HTTP 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 サーバー接続成功後のツール発見フロー:
defs()関数がclient.listTools()を呼び出して、サーバーの宣言済みツールリストを取得します- タイムアウト保護を設定して、応答のないサーバーが全体起動フローをブロックするのを防ぎます
convertMcpTool(mcpTool, client, timeout)が各 MCP ツールを AI SDKdynamicToolに変換します- ツールの名前付け規則:
sanitizedClientName + "_" + sanitizedToolName(例:github_search_repositories) 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() メソッドはコールバックリクエストを処理します:
- URL から
stateとcodeパラメータを抽出します stateがpendingAuthsに存在することを検証します(CSRF 保護)- 対応する
resolve(code)を呼び出して、待機中のauthenticate()コールを起動します - カスタム 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フィールドをチェック;期限切れのトークンは自動的には使用されません
汎用ヘルパー関数 updateField と clearField はタイプセーフなフィールドレベルの更新を提供します:
// 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 のみをサポートしています。自動フォールバックにより両方のタイプのサーバーに接続できます |
| 固定コールバックポート 19876 | OAuth コールバックには redirect_uri に事前登録するために予測可能なポートが必要です。ensureRunning() がポートの競合を処理し、複数の OpenCode インスタンスが同じコールバックサーバーを共有できます |
| 再帰的プロセスクリーンアップ(BFS + リーフ優先) | stdio MCP サーバーは子プロセスのチェーンを起動する可能性があります。親を先にkillすると子が init に再配置されて孤児になります。BFS トラバーサル + リーフからの SIGTERM で完全なプロセスツリーのクリーンアップを保証します |
| 5分の OAuth タイムアウト | OAuth はユーザーがブラウザでマニュアル操作する必要があります。5分はログインと承認を完了するのに十分な時間を与え、無限のハングを防ぎます |
ファイル権限 0o600 | OAuth トークンはパスワードと同等です。0o600(所有者のみ読み書き可)はマルチユーザーシステムでの基本的なセキュリティ対策です |
concurrency: "unbounded" 並列初期化 | MCP サーバーは完全に独立しています。1つが遅くても他に影響しません。コンカレンシー制限なしで全サーバーができるだけ早く準備完了できます |
他のモジュールとの関係
- Agent:Agent は統合ツールインターフェース経由で MCP ツールを呼び出し、組み込みツールと区別できません。
convertMcpTool()が MCP ツールを AI SDKdynamicToolとしてラップし、ツールの説明は自動的に Agent コンテキストに注入されます。Agent のresolveTools()は組み込みツールと MCP ツールを同時に登録します - Config:
Config.mcpが MCP サーバー一覧を定義し、各アイテムはtype(stdio/url)、command、args、env、url、disabledなどのフィールドを含みます。設定変更後、再度 start() を呼び出す必要があります - Instance:MCP 状態は
Instance.state()経由で管理され、プロジェクトインスタンスのライフサイクルに従います。start()/stop()はインスタンスの初期化/シャットダウン時に自動的に呼び出されます。Effect.addFinalizerはインスタンスが破棄されたときに子プロセスがクリーンアップされることを保証します - Bus:
ToolsChangedBusEvent(BusEvent.define("mcp.tools.changed"))がツールリストの変更を通知し、Agent と UI 層が更新をサブスクライブします。BrowserOpenFailedイベントが OAuth ブラウザ起動失敗時のデグラデーションを処理します - Global:
Global.Path.data/mcp-auth.jsonが OAuth トークンを保存します。ファイル権限0o600がセキュリティを保証します。パスは Global モジュールで統一管理されます - Permission:MCP ツールの呼び出しも Permission システムの管理下にあります。ユーザーは特定の MCP ツールの実行を承認または拒否でき、組み込みツールと同じ権限ルールを使用します
- Provider:MCP ツールの実行結果は
dynamicTool経由で Agent に返され、Provider モジュールの LLM コールフローにシームレスに統合されます