Skip to main content
ツールと拡張機能

MCP 統合

MCP(Model Context Protocol)は AI Agent が外部ツールを呼び出すためのオープンプロトコルです。SDK では MCP Server を定義し、Agent にツールを設定できます。接続管理、ツール発見、OAuth、状態同期などのランタイム処理は基盤の CLI が担います。

アーキテクチャ概要

┌────────────────────────────────────────────────────────────┐
│  Your application (SDK Host)                               │
│                                                            │
│   ┌──────────────────────────┐                             │
│   │ createSdkMcpServer(...)  │  ← In-Process tools         │
│   │  + tool(...)             │     defined inline, no proc │
│   └──────────────────────────┘                             │
│                  │                                         │
│                  ▼                                         │
│   ┌──────────────────────────┐                             │
│   │  query({ mcpServers })   │── stdio ─▶ qodercli child  │
│   └──────────────────────────┘                             │
│                                          │                 │
│                                          ├── stdio ──▶ MCP server (process)
│                                          ├── sse   ──▶ MCP server (HTTP/SSE)
│                                          └── http  ──▶ MCP server (Streamable HTTP)
└────────────────────────────────────────────────────────────┘
  • In-Process:ツールはあなた自身のプロセスで動く普通の async 関数(JS / Python)です。server インスタンスは SDK の control channel 経由で CLI と通信し、追加のプロセスは起動しません。
  • External:設定でサブプロセスまたはリモート URL を宣言し、CLI が接続、発見、呼び出しを担当します。

3つの接続方式

方式設定項目 typeプロセス境界適用シナリオ
In-Process'sdk'createSdkMcpServer / create_sdk_mcp_server で作成)同一プロセスホストの状態への直接アクセスが必要なカスタム業務ツール
Stdio'stdio'(省略可能)サブプロセス既存の MCP ツールパッケージ(@modelcontextprotocol/server-*
SSE / HTTP'sse' / 'http'リモートリモートサービス、SaaS ツール、OAuth が必要なサービス
3つの方式は併用できます——同一セッションに異なるタイプのサーバーを複数登録できます。
💡 Python では mcp_serversstr / pathlib.Path も渡せます。JSON 設定ファイルのパスを指すと、SDK が --mcp-config <path> として CLI に透過します。

In-Process Server(推奨)

In-process ツールは最も直接的な拡張方法です。普通の async 関数に schema 宣言を付けるだけで Agent から呼び出せます。ツール作成 / schema / ハンドラーの詳細は Tools を参照してください。このセクションは MCP server の組み立てに関する部分のみを扱います。

30秒で開始

import { query, createSdkMcpServer, tool } from '@qoder-ai/qoder-agent-sdk';
import { z } from 'zod';

const greet = tool(
  'greet',
  'Greet someone.',
  { name: z.string().describe('Recipient name') },
  async ({ name }) => ({
    content: [{ type: 'text', text: `Hello, ${name}!` }],
  }),
);

const server = createSdkMcpServer({
  name: 'my_tools',
  tools: [greet],
});

const q = query({
  prompt: 'Use the greet tool to greet Alice',
  options: {
    mcpServers: { my_tools: server },
    allowedTools: ['mcp__my_tools__greet'],
  },
});

for await (const msg of q) {
  if (msg.type === 'result') console.log(msg.result);
}

完全なシグネチャ

function tool<Schema extends ZodRawShape>(
  name: string,
  description: string,
  inputSchema: Schema,
  handler: (args: z.infer<ZodObject<Schema>>, extra: unknown) => Promise<CallToolResult>,
  extras?: ToolExtras,
): SdkMcpToolDefinition<Schema>;

type ToolExtras = {
  annotations?: ToolAnnotations;  // see "What annotations are actually consumed" below
};

function createSdkMcpServer(options: {
  name: string;       // server name (determines tool prefix mcp__<name>__)
  version?: string;   // defaults to '1.0.0'
  tools?: Array<SdkMcpToolDefinition<any>>;
}): McpSdkServerConfigWithInstance;
パラメータ説明
name(tool)ツール名。完全修飾名は mcp__<server>__<name> になります
descriptionモデル向けの説明。AI がいつ呼び出すかを決定します——What/When を明確に記述
inputSchema / input_schemaTypeScript は Zod raw shape(z.object(...) ではない)。Python はシンプル dict / TypedDict / 完全な JSON Schema dict をサポート
handler実際のロジック。CallToolResult を返します
annotationsMCP ツールアノテーション。詳細は下表参照
name(server)サーバー名(ツールプレフィックス mcp__<name>__ を決定)
versionデフォルト '1.0.0'
toolsツールのリスト
戻り値は { type: 'sdk', name, instance } の形で、そのまま MCP servers 設定に入れられます。
⚠️ 同じ server 設定を複数の query() で再利用しないでください:各 query は独立した transport をバインドします。再利用に副作用はありませんが、「query 間で状態を共有する」機能は得られません——共有状態は handler クロージャの外のモジュールスコープに配置してください。

annotations の実際のサポート状況

以下の3つのフィールドは SDK が実際に消費し、MCP 状態クエリ(TypeScript の mcpServerStatus().tools[i].annotations、Python の get_mcp_status().mcpServers[i].tools[i].annotations)でホスト側にエコーされます:
フィールド動作ホスト側の読み方
readOnlyHintツールが読み取り専用であることを宣言。読み取り専用ツールは並行実行可能(同一バッチ内で相互ブロックしない)。TUI のツール詳細に [read-only] バッジが表示されるannotations.readOnly
destructiveHintツールが破壊的操作を実行することを宣言。TUI のツール詳細に [destructive] バッジが表示されますannotations.destructive
openWorldHintツールが外部世界(Web 検索、サードパーティ API など)に到達することを宣言。TUI のツール詳細に [open-world] バッジが表示されるannotations.openWorld
ホスト側のフィールド名は Hint サフィックスを除いた形です:readOnlyHintannotations.readOnly など。annotations オブジェクトには明示的に設定されたフィールドのみ含まれます。 ⚠️ これら3つのフィールドは auto モードの権限判断に影響しません。CLI は server が自己宣言した annotation を検証不能なヒントとみなし(server は自由に過少/過大宣言できるため)、権限パイプラインに入れません。特定のツールを確実に拒否したい場合は、ツール許可リストまたは hooks で遮断してください——annotation はホスト側の識別と TUI 表示のためだけのものです。
idempotentHinttitle は現在 SDK が消費しません——渡してもエラーにはなりませんが、SDK は消費せず、ホストにもエコーしません。アプリでこれらの情報が必要なら、ホスト側で独自にマッピングを保持してください。
💡 maxResultSizeChars について:Python SDK は ToolAnnotations(maxResultSizeChars=...) でツールの _metaanthropic/maxResultSizeChars を書き込み、CLI がデフォルト 50K の戻り値長制限を緩和します(TS も同名 annotation で公開し、ワイヤ形式は同一)。

CallToolResult 構造

type CallToolResult = {
  content: Array<
    | { type: 'text'; text: string }
    | { type: 'image'; data: string; mimeType: string }     // base64
    | { type: 'audio'; data: string; mimeType: string }
    | { type: 'resource'; resource: { uri: string; text?: string; blob?: string; mimeType?: string } }
    | { type: 'resource_link'; uri: string; title?: string; name?: string }
  >;
  isError?: boolean;  // when true, the AI sees this as a failed result
};
ビジネス上の失敗にはエラーフラグ(isError: true / is_error: True)を使い、例外を投げないでください——例外は tool call 全体を終了させ AI に情報が届きません。エラーフラグなら AI は「この呼び出しは失敗した、別の方法を試そう」と判断できます。Python 側と TS 側の動作差異(resource_link のテキスト降格、トップレベル _meta の非透過など)は Tools を参照してください。
const queryDb = tool(
  'query_db',
  'Read-only SQL query.',
  { sql: z.string() },
  async ({ sql }) => {
    if (!/^\s*SELECT/i.test(sql)) {
      return {
        isError: true,
        content: [{ type: 'text', text: 'Only SELECT statements are allowed' }],
      };
    }
    const rows = await db.query(sql);
    return { content: [{ type: 'text', text: JSON.stringify(rows) }] };
  },
  { annotations: { readOnlyHint: true } },
);

ハンドラーのキャンセルシグナル(Python)

Python のハンドラーは第2引数 ToolInvocationContext を受け取れます。CLI が実行中の呼び出しをキャンセルした際、extra.signal で協調的に終了できます:
@tool("watch", "Watch a counter", {"max": int})
async def watch(args, extra):
    for i in range(args["max"]):
        if extra.signal.is_set():
            return {"content": [{"type": "text", "text": f"aborted at {i}"}]}
        await asyncio.sleep(0.01)
    return {"content": [{"type": "text", "text": "done"}]}

Stdio Server

サブプロセスの stdin/stdout を通じて MCP サーバーと通信します。NPM の @modelcontextprotocol/server-* シリーズはすべて stdio 実装です。
type McpStdioServerConfig = {
  type?: 'stdio';                       // optional; stdio is the default
  command: string;                      // executable command
  args?: string[];                      // command arguments
  env?: Record<string, string>;         // environment variables
  isProxy?: boolean;                    // proxy flag (aggregates multiple backends)
};

const q = query({
  prompt: 'Read the title from the project README',
  options: {
    mcpServers: {
      fs: {
        command: 'npx',
        args: ['-y', '@modelcontextprotocol/server-filesystem', '/path/to/project'],
      },
      gh: {
        command: 'npx',
        args: ['-y', '@modelcontextprotocol/server-github'],
        env: { GITHUB_TOKEN: process.env.GITHUB_TOKEN! },
      },
    },
  },
});
command が到達不能または起動失敗でも query 全体は巻き込まれません——該当 server の status が非 'connected' のままになるだけで、他の server は影響を受けません。

SSE / HTTP Server

type McpSSEServerConfig = {
  type: 'sse';
  url: string;
  headers?: Record<string, string>;
  isProxy?: boolean;
};

type McpHttpServerConfig = {
  type: 'http';                         // Streamable HTTP
  url: string;
  headers?: Record<string, string>;
  isProxy?: boolean;
};

const q = query({
  prompt: 'Query this month\'s sales data',
  options: {
    mcpServers: {
      analytics: {
        type: 'http',
        url: 'https://analytics.example.com/mcp',
        headers: { Authorization: `Bearer ${process.env.ANALYTICS_TOKEN}` },
      },
    },
  },
});
リモート URL が到達不能でも同様に query がハングすることはなく、サーバーステータスは 'connected' 以外となり、他のサーバーには影響しません。OAuth が必要なリモートサービスについては OAuth 認証 を参照してください。

ツール命名とホワイトリスト

CLI がモデルに MCP ツールを公開する際、統一的にプレフィックスを付加します:
mcp__<server_name>__<tool_name>
たとえば server 名 my_tools、tool 名 greet なら、モデルに見えるツール名は mcp__my_tools__greet です。server 名はハイフンなどの特殊文字を含められます(my-toolsmcp__my-tools__<tool>)。

tools:モデルに見えるツールを絞る

モデルに一部のツールだけを見せたい場合は tools を使います。CLI はリストにない組み込みツールをすべて disallow リストに追加します — 実質的な可視性ホワイトリストです:
options: {
  mcpServers: { my_tools: server },
  tools: [
    'Read', 'Grep',                  // built-in tools you still want
    'mcp__my_tools__greet',
    'mcp__my_tools__search_docs',
  ],
}
⚠️ tools を渡さない=すべて開放:すべての組み込みツールと接続済み MCP サーバーのツールがモデルに公開されます。本番環境では明示的に列挙して範囲を絞ることを推奨します。

事前承認リスト(可視性のホワイトリストではない

allowedTools / allowed_tools は列挙したツールを「自動許可」ルールに加えます——呼び出し時に権限プロンプトをスキップしますが、列挙されていないツールが隠されるわけではありません。低リスクの MCP ツールを承認不要にする用途が一般的です:
options: {
  mcpServers: { my_tools: server },
  allowedTools: [
    'mcp__my_tools__greet',          // pre-approved, no prompt
    'mcp__my_tools__search_docs',
  ],
}
事前承認リストを渡さないのは、事前承認ルールがないという意味だけです——モデルは引き続きすべてのツールを見て呼び出せます。書き込み操作が権限モードに従って承認フローを通るだけです。完全なセマンティクスは Permissions ドキュメント を参照してください。

プロセス型 server のホワイトリスト

allowedMcpServerNames / allowed_mcp_server_namesプロセス型(stdio/sse/http)サーバーのみをフィルタし、in-process サーバーには影響しませんstrictMcpConfig: true / strict_mcp_config=True と組み合わせると、CLI がローカルの追加設定を読み込むのを拒否できます:
options: {
  mcpServers: {
    keep: makeStdioConfig('...'),
    drop: makeStdioConfig('...'),
  },
  allowedMcpServerNames: ['keep'],   // 'drop' still appears in status but does not connect
  strictMcpConfig: true,             // skip loading MCP servers from settings.json / .mcp.json
}
⚠️ このホワイトリストを渡さないと全開放になります:宣言されたすべてのプロセス型 server が接続されます。絞りたい場合は明示的に列挙してください。in-process server はこのフィールドの影響を受けません。

ランタイム管理

TypeScript のランタイム管理は query() が返す Query オブジェクトを通じて行います。Python では単発の query() イテレーターは途中で server や認証を変更できないため、QoderSDKClient を使う必要があります。すべてのメソッドは control channel 経由で CLI と通信し、非同期かつ冪等です。
⚠️ キャッシュ原則:MCP server 設定 / 認証状態の変更は tools リストを再構築し、セッション途中の変更は prompt prefix キャッシュを壊します。SDK は「状態のクエリ + 最初のメッセージ前の認証完了」のためのメソッドを提供します。server セット自体は起動時に options で一括設定し、変更が必要ならセッションを再起動してください。

状態の照会

const status = await q.mcpServerStatus();
// Returns McpServerStatus[], each item includes:
//   { name, status: 'pending' | 'connecting' | 'connected' | 'failed' | 'needs-auth' | 'disabled', tools?, ... }

for (const s of status) {
  console.log(`${s.name}: ${s.status}`);
  if (s.status === 'connected') {
    console.log('  tools:', s.tools?.map((t) => t.name));
  }
}
💡 MCP ハンドシェイクは CLI の initialize 完了後、最初のユーザーメッセージ前に行われます。初期化結果が返った後に status を照会しないと実際の結果は得られません——ハンドシェイク IO は数百ミリ秒かかることがあるため、connected になるまでポーリングすることを推奨します。

状態変化のサブスクリプション

  • TypeScript:MCP 状態はプッシュではなくプルです——await q.mcpServerStatus() を呼び、必要なら自分のコードでポーリングします。
  • Python:プルに加えて、options に on_mcp_status_change コールバックを設定でき、状態変化のたびに1回呼ばれます。あるいはメッセージストリームで system/mcp_status_change をフィルタしてもかまいません。コールバックとストリームは同じ payload です。
async def on_status(msg):
    print(f"{msg['server_name']} -> {msg['status']}")
    if msg.get("error"):
        print("  error:", msg["error"])


options = QoderAgentOptions(
    mcp_servers={...},
    on_mcp_status_change=on_status,
)

server セットの変更

prompt prefix キャッシュを安定させるため、server セットの変更は起動時に一括で済ませることを推奨します:
やりたいことTypeScriptPython
server の追加 / 削除 / 置換options.mcpServers で設定。セット変更時は query() を再起動起動時に mcp_servers を設定。ランタイムでは client.set_mcp_servers(servers) で全量置換({added, removed, errors} を返す)
一部のプロセス型 server のみ有効化allowedMcpServerNames ホワイトリストallowed_mcp_server_names ホワイトリスト
特定 server の再接続query() を再起動client.reconnect_mcp_server(name)。主に 'failed' 状態からの復旧に使用
server の有効化 / 無効化query() を再起動client.toggle_mcp_server(name, enabled)。無効化すると切断され、そのツールが下ろされる
特定 server からのサインアウトtoken なしで query() を再起動client.mcp_clear_auth(name)
⚠️ Python のこれらのランタイムメソッドはいずれも tools リストを再構築するため、prompt prefix キャッシュを壊します。本番環境では起動時にすべて設定し、これらの API はデバッグとローカル開発用に留めてください。

コントロールリクエストのタイムアウト

options: {
  controlRequestTimeoutMs: 20_000,  // default 60_000; pass 0 to disable
}
タイムアウト後、SDK は自動的に control_cancel_request を書き込み、保留中のリクエストを reject します。

OAuth 認証

リモート MCP サーバー(HTTP/SSE)は OAuth を必要とすることがよくあります。CLI には完全な OAuth 2.0 + PKCE + Dynamic Client Registration(RFC 7591)実装が内蔵されています。
⚠️ キャッシュ原則:OAuth 完了後、CLI は server に再接続し tools を再発見します——セッション途中の認証完了は必ず prompt prefix キャッシュを壊します。最初のユーザーメッセージ送信に認証を完了し、tools リストが安定してから会話を始めることを推奨します。
💡 このセクションは CLI 主導の OAuth のみを扱います:CLI 自身が metadata discovery、PKCE、token 交換、token 永続化を行います。もう1つサーバー主導の認証経路があります——server が MCP elicitation/create で client をある URL に誘導して認可を完了させる方式です(典型例:GitHub MCP)。2つの経路は独立しており、同時にトリガーされることはありません。詳細は Elicitation:サーバーによるユーザー入力要求 を参照してください。

ホスト主導の認証(outbound)

ホスト自身が OAuth のタイミングを制御し、最初のユーザーメッセージを送信する前に完了します:
const q = query({
  prompt: userMessages(),  // AsyncIterable — no message is sent yet
  options: {
    mcpServers: {
      // Assume this remote server uses the CLI-driven standard OAuth (metadata discovery + PKCE).
      // If you connect to a server like GitHub MCP that implements OAuth on its own side, use onElicitation instead.
      analytics: { type: 'http', url: 'https://analytics.example.com/mcp' },
    },
  },
});

// Wait for handshake to complete
await q.initializationResult();

// Find servers that need authentication
const status = await q.mcpServerStatus();
for (const s of status.filter((x) => x.status === 'needs-auth')) {
  const result = await q.mcpAuthenticate(s.name);
  if (result.requiresUserAction) {
    await openInBrowser(result.authUrl!);
    const callbackUrl = await waitForUserPasteCallback();
    await q.mcpSubmitOAuthCallbackUrl(s.name, callbackUrl);
  }
  // Silent path (cached client + valid refresh token): result.requiresUserAction === false
  // No UI prompt needed; just proceed to the next step.
}

// At this point the tools list is stable; sending the first user message
// will let the prompt prefix cache be established cleanly.
for await (const msg of q) { /* ... */ }
メソッド(TypeScript / Python)用途呼び出しタイミング
mcpAuthenticate(name, redirectUri?) / mcp_authenticate(name, redirect_uri=None)OAuth を開始。{ authUrl?, requiresUserAction } を返す。サイレント更新成功時は requiresUserAction: false で UI 不要最初のユーザーメッセージ前
mcpSubmitOAuthCallbackUrl(name, url) / mcp_submit_oauth_callback_url(name, callback_url)完全なコールバック URL(code/state 含む)を提出最初のユーザーメッセージ前
inject_mcp_token(name, token)(Python のみ)ホストが OAuth 全体を自前で実行し、OAuthToken を CLI に直接注入最初のユーザーメッセージ前
mcp_clear_auth(name)(Python のみ)CLI 保存の OAuth 資格情報を削除。「サインアウト」に相当いつでも。次のツール呼び出しで再認証がトリガーされる
redirectUri / redirect_uri は省略可能で、デフォルトの OAuth コールバック先(Electron カスタムプロトコル、社内ネットワークのコールバックアドレスなど)を上書きします。 CLI はデフォルトでトークンをシステム Keychain(macOS / Linux Secret Service)に保存し、フォールバックとして ~/.qoder/mcp-oauth-tokens.json(0o600 パーミッション + クロスプロセスロック)を使用します。

Inbound:on_mcp_oauth_required コールバック(Python)

Python SDK は inbound 経路もサポートします:CLI がハンドシェイク中に server の OAuth 必要性を検出すると、control_request 経由で McpOAuthRequest を SDK にプッシュし、SDK はホストの on_mcp_oauth_required コールバックを呼びます。ホストは以下のいずれかの resolution を返します:
戻り値の型意味
OAuthToken または {"token": OAuthToken}ホストが OAuth フロー全体を完了し、token を直接 CLI に注入
{"callbackUrl": "..."}ホストは完全なコールバック URL(code / state を含む)のみを取得し、CLI が解析して token に交換
{"code": "...", "state": "..."}ホスト自身が code を解析し、CLI に直接返す
None拒否。CLI は該当サーバーを failed としてマーク
async def handle_oauth(request: McpOAuthRequest) -> McpOAuthResolution | None:
    # Open request['auth_url'] in an Electron BrowserWindow / system browser
    callback_url = await open_browser_and_wait_for_callback(request["auth_url"])
    return {"callbackUrl": callback_url}


options = QoderAgentOptions(
    mcp_servers={"analytics": {"type": "http", "url": "https://analytics.example.com/mcp"}},
    on_mcp_oauth_required=handle_oauth,
    control_request_timeout_ms=120_000,   # user authorization may take a while
)

Elicitation:サーバーによるユーザー入力要求

MCP elicitation/createserver → client 方向のリクエストで、client にユーザーへのインタラクション表示を求めます。SDK はこれを onElicitation(TypeScript)/ on_elicitation(Python)でホストに公開します。

2つのモード

モードトリガーシナリオ典型的な用途
'form'サーバーが構造化入力を求める。リクエストに requestedSchema(MCP 制限サブセットの JSON Schema)が付くAPI key 入力、設定項目の記入、二次確認
'url'サーバーがユーザーをある URL に誘導する。リクエストに url + elicitationId が付くサーバー自前の OAuth、デバイスコード認証、アカウント連携
URL モードは非同期で完了します:server が自身のコールバックでユーザーの認可を受け取ると notifications/elicitation/complete を送信し、SDK はそれを elicitation complete メッセージとしてメッセージストリームに投影します。
⚠️ qodercli は現在 MCP capability 宣言で elicitation: {} のみを送信します({ form: {} } と等価)。そのため現時点でリモート server から client に到達するのは form モードのみです。URL モードはプロトコル層では完全ですが、CLI が elicitation.url capability を明示的に宣言する必要があります——今後の CLI バージョンで対応され次第、この経路は自動的に開通します。

コールバックシグネチャ

import type { OnElicitation, ElicitationRequest, ElicitationResult } from '@qoder-ai/qoder-agent-sdk';

type OnElicitation = (
  request: ElicitationRequest,
  options: { signal: AbortSignal },
) => Promise<ElicitationResult>;

type ElicitationRequest = {
  serverName: string;          // name of the MCP server that issued the request
  message: string;             // explanation shown to the user
  mode?: 'form' | 'url';       // defaults to form
  url?: string;                // required when mode='url'
  elicitationId?: string;      // required when mode='url'; used to correlate later completion notifications
  requestedSchema?: Record<string, unknown>;  // field schema carried when mode='form'
  title?: string;
  displayName?: string;
  description?: string;
};

type ElicitationResult = {
  action: 'accept' | 'decline' | 'cancel';
  content?: Record<string, string | number | boolean | string[]>;  // populated when accept + form
};
Python の注意事項:
  • フィールド名は TS SDK の camelCase に従います(serverName / elicitationId / requestedSchema / displayName)。CLI の snake_case payload は SDK が自動的に変換します。
  • None を返すと {"action": "cancel"} と等価です。コールバック未設定時、SDK はデフォルト契約に従い自動的に cancel を返します。
  • mcp.types.ElicitResult Pydantic モデルを返すこともできます(SDK が model_dump を呼び出します)。
TypeScript では signalq.close() / 中断時に abort されるため、長いフローではチェックしてください。

form モードの例

const q = query({
  prompt: userMessages(),
  options: {
    mcpServers: { my_server: { type: 'http', url: '...' } },
    onElicitation: async (request) => {
      if (request.mode !== 'url' && request.requestedSchema) {
        // Show a form in the UI and collect the user's input
        const filled = await showForm(request.message, request.requestedSchema);
        if (!filled) return { action: 'cancel' };
        return { action: 'accept', content: filled };
      }
      return { action: 'decline' };
    },
  },
});

URL モードの例(elicitation_complete との連携)

const q = query({
  prompt: userMessages(),
  options: {
    mcpServers: { gh: { type: 'http', url: 'https://mcp.github.com/mcp' } },
    onElicitation: async (request, { signal }) => {
      if (request.mode !== 'url' || !request.url) {
        return { action: 'cancel' };
      }
      // Open the browser so the user can authorize; we only acknowledge "I have started the flow"
      // Real completion is signaled by notifications/elicitation/complete from the server side
      await openInBrowser(request.url);
      return { action: 'accept' };
    },
  },
});

// Listen for system/elicitation_complete to learn when server-side authorization is done
for await (const msg of q) {
  if (msg.type === 'system' && msg.subtype === 'elicitation_complete') {
    console.log(`server '${msg.mcp_server_name}' finished elicitation ${msg.elicitation_id}`);
    // The server now has its token; subsequent tool calls can succeed directly.
  }
}
💡 elicitation コールバック内でブラウザーの戻りを await しないでください。URL モードの設計では、コールバックは即座に accept(=ユーザーがフローを開始した)を返し、CLI は control チャンネルをブロックしません。本当の「完了」シグナルは後続の elicitation_complete メッセージです。OAuth リダイレクト全体を await すると、control リクエストのタイムアウトが発生します。

OAuth パスとの境界

  • CLI 主導 OAuthmcpAuthenticate / mcp_authenticate など):token は qodercli の Keychain に保存。MCP 状態が needs-auth のときに駆動。elicitation コールバックはトリガーされない
  • サーバー主導 elicit URL:token は server 内部に保存。MCP 状態が needs-auth になることはない。elicitation コールバックで受け止め、elicitation_complete メッセージで完了。
2つの経路は排他ではありませんが重複もしません:同一 server は通常どちらか一方だけを使います。どちらか分からない場合:ハンドシェイク時に client へ elicitation/create を送るかどうかを見てください——送るならサーバー主導です。

Hook チャネル

ホストは hooks でも elicitation を観察 / インターセプトできます:
Hook イベントタイミング説明
Elicitationserver リクエスト到達時TypeScript では onElicitation より優先され、自動 accept / decline / cancel(UI の短絡)または素通しが可能。Python では読み取り専用の観察チャンネルで、判断は on_elicitation が担う
ElicitationResultユーザー応答後TypeScript は action / content の書き換えや block が可能。Python は読み取り専用
Notification(type=elicitation_completeURL モード完了通知の到達時IDE / システム通知のトリガー
from qoder_agent_sdk import HookMatcher, QoderAgentOptions


async def on_elicit(input, tool_use_id, context):
    print(
        "elicit from",
        input["mcp_server_name"],
        "mode=",
        input["mode"],
        "schema=",
        input.get("requested_schema"),
    )
    return {"continue_": True}


options = QoderAgentOptions(
    mcp_servers={"my_server": {"type": "http", "url": "..."}},
    hooks={
        "Elicitation": [HookMatcher(hooks=[on_elicit])],
    },
)

Options クイックリファレンス

フィールド(TypeScript / Python)デフォルト説明
mcpServers / mcp_serversサーバー名 → 設定。Python は JSON 設定ファイルパスも受け付ける
allowedMcpServerNames / allowed_mcp_server_namesプロセス型サーバーのホワイトリスト(in-process には影響しない)。省略すると全開放
strictMcpConfig / strict_mcp_configfalseCLI がユーザー設定ファイルから追加の MCP を読み込むのを禁止
tools / toolsモデル可視ツールのホワイトリスト。省略すると組み込み + MCP ツールすべてが可視
allowedTools / allowed_tools事前承認リスト(権限プロンプトをスキップ、可視性は制御しない)。省略すると事前承認ルールなし
disallowedTools / disallowed_tools明示的に拒否するツール。allow より優先
controlRequestTimeoutMs / control_request_timeout_ms60_000control リクエストのタイムアウト(mcp 系を含む)。0 で無効化
onElicitation / on_elicitationMCP server がユーザー入力を要求したときに発火(form / url の2モード)
on_mcp_oauth_required(Python のみ)CLI が server の OAuth 必要性を検出したときに発火
on_mcp_status_change(Python のみ)server の状態変化ごとに発火。system/mcp_status_change ストリームのフィルタと等価

ランタイムメソッド早見表

メソッド(TypeScript / Python)説明呼び出しタイミング
mcpServerStatus() / get_mcp_status()現在のすべての MCP サーバー状態を取得いつでも
mcpAuthenticate(...) / mcp_authenticate(...)OAuth を能動的に開始。{ authUrl?, requiresUserAction } を返す最初のユーザーメッセージ前
mcpSubmitOAuthCallbackUrl(...) / mcp_submit_oauth_callback_url(...)OAuth コールバックを提出最初のユーザーメッセージ前
set_mcp_servers(servers)(Python のみ)MCP server 設定を全量置換。{added, removed, errors} を返すいつでも(prefix キャッシュを壊す)
reconnect_mcp_server(name)(Python のみ)指定 server を再接続いつでも
toggle_mcp_server(name, enabled)(Python のみ)server の有効化 / 無効化いつでも
inject_mcp_token(name, token)(Python のみ)ホストが OAuth 完走後に token を注入最初のユーザーメッセージ前
mcp_clear_auth(name)(Python のみ)保存済み OAuth 資格情報を削除いつでも
TypeScript では server セットの増減・変更は options.mcpServers(起動時設定)+ query() の再起動で行ってください。

型リファレンス

import type {
  // Factory function return value
  McpSdkServerConfigWithInstance,
  // Union type — pass into options.mcpServers
  McpServerConfig,
  // Individual transport types
  McpStdioServerConfig,
  McpSSEServerConfig,
  McpHttpServerConfig,
  McpSdkServerConfig,
  // Status
  McpServerStatus,
  McpServerStatusConfig,
  // Elicitation
  OnElicitation,
  ElicitationRequest,
  ElicitationResult,
  SDKElicitationCompleteMessage,
} from '@qoder-ai/qoder-agent-sdk';

import { tool, createSdkMcpServer } from '@qoder-ai/qoder-agent-sdk';
import type {
  AnyZodRawShape,
  InferShape,
  SdkMcpToolDefinition,
} from '@qoder-ai/qoder-agent-sdk';
McpServerStatus.status の列挙値:
意味
'pending'登録済み、接続未開始
'connecting'ハンドシェイク中
'connected'接続済み、ツール呼び出し可能
'failed'接続失敗(error フィールドを参照)
'needs-auth'OAuth が必要、認証フローを実行してください
'disabled'無効化(CLI 内部設定または外部状態による)

ベストプラクティス

  1. 説明は AI に向けて書く:ツールの description が AI の選択タイミングを決めます。「何をするか、いつ使うか、何に使うべきでないか」を明確に。
  2. フィールドに説明を付ける:TypeScript の Zod フィールドには必ず .describe(...)、Python は Annotated[type, "..."] を。AI はこれらの情報で呼び出し引数を構築します。
  3. 失敗はエラーフラグで、例外を投げない:AI に結果を見せましょう。例外はモデルを混乱させ、リトライを誘発することがあります。
  4. 読み取り専用 + readOnlyHint を優先:書き込み操作は慎重に。権限コールバックまたは hooks による二次確認を併用してください。
  5. サーバー名は短く:ツールプレフィックスに含まれるため、長い名前はトークンを浪費します。
  6. In-process の共有状態はモジュールスコープに配置:handler はクロージャですが、各 query は同じ server インスタンスを再利用します。
  7. OAuth は最初のユーザーメッセージ前に完了:セッション途中の認証完了は必ず prompt prefix キャッシュを壊します。
  8. MCP 状態は必要に応じてプル:TypeScript は mcpServerStatus() でポーリング、Python は get_mcp_status() または on_mcp_status_change コールバックを選択。
  9. control リクエストタイムアウトを適切に設定:リモートサーバーのハンドシェイクは秒単位になることがあります。デフォルト 60 秒で通常は十分ですが、OAuth のユーザー操作待ちでは大きめに、CI 環境では明示的に設定を。
  10. strict MCP config で隔離:ユーザーローカルの settings.json / .mcp.json で宣言された MCP サーバーがアプリに干渉しないようにします。

完全な例

import { query, createSdkMcpServer, tool } from '@qoder-ai/qoder-agent-sdk';
import { z } from 'zod';

// 1. Define business tools
const getUserOrders = tool(
  'get_user_orders',
  'Query a user\'s orders, optionally filtered by status.',
  {
    userId: z.string().describe('User UUID'),
    status: z.enum(['pending', 'paid', 'shipped', 'cancelled']).optional()
      .describe('Filter by order status'),
  },
  async ({ userId, status }) => {
    try {
      const orders = await db.getOrders(userId, status);
      return { content: [{ type: 'text', text: JSON.stringify(orders) }] };
    } catch (err) {
      return {
        isError: true,
        content: [{ type: 'text', text: `Query failed: ${(err as Error).message}` }],
      };
    }
  },
  { annotations: { readOnlyHint: true } },
);

// 2. Assemble the server
const myServer = createSdkMcpServer({
  name: 'crm',
  tools: [getUserOrders /* , ... */],
});

// 3. Start query (use AsyncIterable so no message is sent yet)
async function* userMessages() {
  yield {
    type: 'user' as const,
    message: { role: 'user' as const, content: 'List the recently paid orders for user-123' },
    parent_tool_use_id: null,
  };
}

const q = query({
  prompt: userMessages(),
  options: {
    mcpServers: {
      crm: myServer,
      // Assume a remote server that uses CLI-driven OAuth (GitHub MCP uses elicit-URL, not this path)
      analytics: { type: 'http', url: 'https://analytics.example.com/mcp' },
    },
    allowedTools: ['mcp__crm__get_user_orders'],
    controlRequestTimeoutMs: 30_000,
  },
});

// 4. Wait for handshake; actively drive auth before the first user message
await q.initializationResult();
const status = await q.mcpServerStatus();
for (const s of status.filter((x) => x.status === 'needs-auth')) {
  const result = await q.mcpAuthenticate(s.name);
  if (result.requiresUserAction) {
    const callbackUrl = await openInBrowserAndWaitForCallback(result.authUrl!);
    await q.mcpSubmitOAuthCallbackUrl(s.name, callbackUrl);
  }
  // Silent refresh success: requiresUserAction === false; no UI required
}

// 5. Consume messages (tools list is now stable; prompt prefix cache will be established correctly)
for await (const msg of q) {
  if (msg.type === 'result') {
    console.log(msg.subtype === 'success' ? msg.result : msg);
    break;
  }
}

await q.close?.();