メインコンテンツへスキップ

Documentation Index

Fetch the complete documentation index at: https://docs.qoder.com/llms.txt

Use this file to discover all available pages before exploring further.

MCP(Model Context Protocol)は AI エージェントが外部ツールを呼び出すためのオープンプロトコルです。SDK を使って MCP Server を定義し、Agent にツールを装備できます。接続管理やツール発見などのランタイム処理は底層の CLI が担います。

アーキテクチャ概要

┌────────────────────────────────────────────────────────────┐
│  Your Node.js 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:ツールは単なる JS 関数で、自分のプロセス内で実行されます。McpServer インスタンスは SDK の control channel を通じて CLI と通信し、別のサブプロセスは起動しません。
  • External:設定でサブプロセスまたはリモート URL を宣言し、CLI が接続、発見、呼び出しを担当します。

3つの接続方式

方式設定項目 typeプロセス境界適用シナリオ
In-Process'sdk'createSdkMcpServer で作成)同一プロセスカスタムビジネスツール、ホスト状態への直接アクセスが必要
Stdio'stdio'(省略可能)サブプロセス既存の MCP ツールパッケージ(@modelcontextprotocol/server-*
SSE / HTTP'sse' / 'http'リモートリモートサービス、SaaS ツール、OAuth が必要なサービス
3つの方式は混在可能です——同じ query() 内で複数の異なるタイプのサーバーを同時に登録できます。

In-Process Server(推奨)

In-process ツールは最も直接的な拡張方法です:通常の async 関数を定義し、Zod スキーマを追加するだけで、エージェントから呼び出し可能になります。

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);
}

tool() 完全なシグネチャ

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
};
パラメータ説明
nameツール名。完全修飾名は mcp__<server>__<name> になります
descriptionモデル向けの説明。AI がいつ呼び出すかを決定します——What/When を明確に記述
inputSchemaZod raw shape(z.object(...) ではなく、フィールドオブジェクトを渡します)
handler実際のロジック。CallToolResult を返します
extras.annotationsMCP ツールアノテーション。詳細は下表参照

annotations の実際のサポート状況

以下の 3 つのフィールドは SDK が実際に消費し、mcpServerStatus().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 オブジェクトには明示的に設定されたフィールドのみが含まれます。 ⚠️ これらのフィールドは auto モードのパーミッション判定には影響しません。CLI はサーバーが自己宣言した annotation を検証不可能な参考情報として扱い(サーバーは過少・過大に申告できるため)、パーミッションパイプラインには敢えて取り込みません——取り込むとサーバーの自己評価に権威を付与してしまうためです。特定ツールを確実にブロックしたい場合は allowedTools ホワイトリストまたは hooks を使用してください——annotation はホスト側での識別(mcpServerStatus)と TUI 表示のためだけのものです。
idempotentHinttitle は現在サポートされていません——渡してもエラーにはなりませんが、SDK は消費せず、ホストにも返しません。アプリケーションでこれらの情報が必要な場合は、ホスト側で独自にマッピングを管理してください。

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 を使用してください——例外はツール呼び出し全体を終了させ、AI は情報を得られません。isError は AI に「この呼び出しは失敗した。別の方法を試してください」と伝えます。
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 } },
);

createSdkMcpServer() 完全なシグネチャ

function createSdkMcpServer(options: {
  name: string;       // server name (determines tool prefix mcp__<name>__)
  version?: string;   // defaults to '1.0.0'
  tools?: Array<SdkMcpToolDefinition<any>>;
}): McpSdkServerConfigWithInstance;
戻り値は { type: 'sdk', name, instance } の形式で、そのまま options.mcpServers に渡せます。
⚠️ 同じ server 設定を複数の query() で再利用しないでください:各 query は独立した transport をバインドします。再利用に副作用はありませんが、「query 間で状態を共有する」機能は得られません——共有状態は handler クロージャの外のモジュールスコープに配置してください。

複数ツールの例

const searchDocs = tool(
  'search_docs',
  'Search internal docs and return relevant snippets.',
  {
    query: z.string().describe('Search keywords'),
    maxResults: z.number().int().min(1).max(20).optional()
      .describe('Maximum number of results, defaults to 5'),
  },
  async ({ query, maxResults = 5 }) => {
    const hits = await docs.search(query, maxResults);
    return { content: [{ type: 'text', text: JSON.stringify(hits) }] };
  },
  { annotations: { readOnlyHint: true } },
);

const server = createSdkMcpServer({
  name: 'kb',
  tools: [searchDocs, queryDb /* ... */],
});

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! },
      },
    },
  },
});

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}` },
      },
    },
  },
});
OAuth が必要なリモートサービスについては OAuth 認証 を参照してください。

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

CLI がモデルに MCP ツールを公開する際、統一的にプレフィックスを付加します:
mcp__<server_name>__<tool_name>
例えばサーバー名 my_tools、ツール名 greet の場合、モデルが認識するツール名は mcp__my_tools__greet です。

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:事前承認(可視性のホワイトリストではない

allowedTools はリストされたツールを「常に許可」ルールに追加します — 呼び出し時に承認プロンプトをスキップしますが、リストにないツールは非表示にはなりません。低リスクの MCP ツールを無人承認したい場合に使います:
options: {
  mcpServers: { my_tools: server },
  allowedTools: [
    'mcp__my_tools__greet',          // pre-approved, no prompt
    'mcp__my_tools__search_docs',
  ],
}
allowedTools を渡さない場合は事前承認ルールが無いだけで、モデルは依然としてすべてのツールを見て呼び出せます。書き込み系操作は通常の permissionMode 承認フローに従います。完全なセマンティクスは Permissions ドキュメントを参照してください。

allowedMcpServerNames:プロセス型サーバーのホワイトリスト

プロセス型(stdio/sse/http)サーバーのみをフィルタリングし、in-process サーバーには影響しませんstrictMcpConfig: 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
}
⚠️ allowedMcpServerNames を渡さない=すべて接続:宣言されたすべてのプロセス型サーバーが接続されます。範囲を絞りたい場合は明示的に列挙してください。in-process サーバーは常にこのフィールドの影響を受けません。

ランタイム管理(Query API)

query() が返す Query オブジェクトには MCP 関連のメソッドがいくつかあります。すべてのメソッドは control channel を通じて CLI と通信し、動作は非同期かつ冪等です。
⚠️ キャッシュの原則:MCP server の設定/認証状態の変更は tools リストを再構築し、セッション途中の変更はプロンプトプレフィックスキャッシュを破壊します。SDK は「状態を照会 + 最初のメッセージ前に認証を完了する」メソッドを提供します。server セット自体は options.mcpServers で起動時に一度設定し、必要に応じて query() を再起動してください。

状態の照会

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 を完了した後、最初のユーザーメッセージの前に発生します。initializationResult() が返された後に status を照会して初めて実際の結果を取得できます——ハンドシェイク IO には数百ミリ秒かかる場合があるため、pollUntilconnected を待ってから使用することを推奨します。

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

MCP 状態はプッシュではなくプルで取得します:await q.mcpServerStatus() を呼び出すだけです。ポーリングが必要な場合は自分のコードで実装してください。

server セットの変更

プロンプトプレフィックスキャッシュの安定性を確保するため、server セットの変更は起動時に一度で完了します:
やりたいこと方法
サーバーの追加/削除/置換options.mcpServers で設定;セットの変更が必要な場合は query() を再起動
プロセス型 server の一部のみ有効化options.allowedMcpServerNames ホワイトリスト
特定 server への再接続query() を再起動(再接続はツールを再発見し、キャッシュに影響)
特定 server からのログアウトquery() 再起動時にそのトークンを含めない;または外部の認証情報ストアをクリアしてから再起動

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

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

OAuth 認証

リモート MCP サーバー(HTTP/SSE)は OAuth を必要とすることがよくあります。CLI には完全な OAuth 2.0 + PKCE + Dynamic Client Registration(RFC 7591)実装が内蔵されています。
⚠️ キャッシュの原則:OAuth 完了後、CLI は server に再接続してツールを再発見し、セッション途中での認証完了は必然的にプロンプトプレフィックスキャッシュを破壊します。そのため「能動的駆動」モード認証のみをサポートしています——最初の streamInput の前にすべての認証を完了し、tools リストが安定してから最初のユーザーメッセージを送信してください。
💡 このセクションは CLI 主導の OAuth のみをカバーします:CLI 自身が metadata discovery、PKCE、token 交換、token 永続化を行います。別のサーバー主導の認証パスもあります——server が MCP elicitation/create を使用してクライアントを特定の URL にリダイレクトさせて認証を完了させます(典型例:GitHub MCP)。2つのパスは独立しており、同時にトリガーされることはありません:サーバー主導の場合 mcpServerStatus()needs-auth を示さず、mcpAuthenticate を呼ぶべきではなく、ホストは onElicitation でリクエストを受け取ります。詳細は Elicitation:サーバーによるユーザー入力要求 を参照してください。
ホスト自身が 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) { /* ... */ }
Pull メソッド用途呼び出しタイミング
mcpAuthenticate(name, redirectUri?)OAuth を開始;{ authUrl?, requiresUserAction } を返す。サイレント更新成功時は requiresUserAction: false で UI 不要最初の streamInput
mcpSubmitOAuthCallbackUrl(name, url)完全なコールバック URL(code/state を含む)を送信最初の streamInput
redirectUri はオプションで、デフォルトの OAuth コールバック先を上書きします(Electron カスタムプロトコル、社内ネットワークコールバックアドレスなど)。 CLI はデフォルトでトークンをシステム Keychain(macOS / Linux Secret Service)に保存し、フォールバックとして ~/.qoder/mcp-oauth-tokens.json(0o600 パーミッション + クロスプロセスロック)を使用します。

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

MCP elicitation/createserver → client 方向のリクエストで、クライアントにユーザーの前でインタラクションを表示させるためのものです。SDK はこのリクエストを Options.onElicitation を通じてホストに公開します。

2つのモード

モードトリガーシナリオ典型的な用途
'form'サーバーが構造化入力を要求し、リクエストに requestedSchema(MCP 制限サブセットの JSON Schema)を含むAPI キー入力、設定項目の入力、二次確認
'url'サーバーがユーザーを特定の URL に誘導して操作を完了させ、リクエストに url + elicitationId を含むサーバー独自の OAuth、デバイスコードアクティベーション、アカウント連携
URL モードは非同期で完了します:server は自身のコールバックでユーザー認可を受信した後、notifications/elicitation/complete を送信します——SDK はこれを SDKElicitationCompleteMessage として Query のメッセージストリームにプッシュします。

コールバックシグネチャ

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
};
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.
  }
}
💡 onElicitation 内でブラウザのリダイレクトを await しないでください。URL モードの設計は:コールバックは即座に accept を返し(=ユーザーがフローを開始した)、CLI は control チャネルをブロックしません。真の「完了」シグナルは後続の elicitation_complete メッセージから届きます。OAuth リダイレクト全体を await すると、control リクエストタイムアウト(controlRequestTimeoutMs)がトリガーされます。

OAuth パスとの境界

  • CLI 主導の OAuthmcpAuthenticate / mcpSubmitOAuthCallbackUrl):トークンは qodercli Keychain に保存;mcpServerStatus()needs-auth の時に駆動;onElicitation はトリガーされません
  • サーバー主導の elicit URL:トークンは server 内部;mcpServerStatus()needs-auth を示しません;onElicitation で受け取り、system/elicitation_complete で完了。
2つのパスは相互排他ではありませんが重複もしません:同じ server は通常どちらか一方のみを使用します。特定の server がどちらを使用するか不明な場合:ハンドシェイク時にクライアントに elicitation/create を送信するかどうかを確認してください——送信した場合はサーバー主導です。

Hook チャネル

ホストは settings.json で hooks を設定して elicitation をインターセプトすることもでき、その動作は onElicitation よりも優先されます:
Hook イベントタイミングできること
Elicitationserver リクエスト到着時、onElicitation の前自動 accept / decline / cancel(UI をショートカット)、または通過
ElicitationResultユーザー応答後action / content の書き換え、またはブロック(強制 decline)
Notification (type=elicitation_complete)URL モード完了通知到着時IDE / システム通知のトリガー
⚠️ qodercli 0.2.x は MCP capability 宣言で elicitation: {}(空オブジェクト、Spring AI Java MCP SDK 互換)のみを送信します。MCP SDK サーバー側はこれを { form: {} } と同等に解釈するため、現在は form モードのみがリモート server からクライアントに到達可能です。URL モードはプロトコル層では完全ですが、CLI が明示的に elicitation.url を宣言して初めて server 側の elicitInput({ mode: 'url' }) がバリデーションを通過します——CLI バージョンの進化に伴い対応予定です。

Options クイックリファレンス

フィールドデフォルト説明
mcpServersRecord<string, McpServerConfig>サーバー名 → 設定
allowedMcpServerNamesstring[]プロセス型サーバーのホワイトリスト(in-process には影響なし);渡さない=すべて開放
strictMcpConfigbooleanfalseCLI がユーザー設定ファイルから追加 MCP を読み込むことを禁止
toolsstring[]モデルに見えるツールのホワイトリスト;渡さない=すべての組み込み + MCP ツールが見える
allowedToolsstring[]事前承認リスト(承認プロンプトをスキップ;可視性は制御しない);渡さない=事前承認ルール無し
disallowedToolsstring[]明示的な拒否リスト;allow よりも優先
controlRequestTimeoutMsnumber60_000control リクエストタイムアウト(mcp 系列を含む)、0 で無効化
onElicitationOnElicitationMCP server がユーザー入力を能動的に要求した際にトリガー(form / url の2モード)。詳細は Elicitation 参照

Query 上のメソッド

メソッド説明呼び出しタイミング
mcpServerStatus()現在のすべての MCP サーバー状態を取得任意のタイミング
mcpAuthenticate(name, redirectUri?)能動的に OAuth を開始;{ authUrl?, requiresUserAction } を返す最初の streamInput
mcpSubmitOAuthCallbackUrl(name, url)OAuth コールバックを送信最初の streamInput
server セットの追加・削除・変更は options.mcpServers(起動時設定)+ query() の再起動で完了してください。詳細は server セットの変更 を参照。

型リファレンス

import type {
  // Factory function return value
  McpSdkServerConfigWithInstance,
  // Union type — pass into options.mcpServers
  McpServerConfig,
  // Individual transport types
  McpStdioServerConfig,
  McpSSEServerConfig,
  McpHttpServerConfig,
  McpSdkServerConfig,
  // Status
  McpServerStatus,
  McpServerStatusConfig,
  // OAuth
  // (OAuthToken / McpOAuthRequest / McpOAuthResolution are exposed via coreTypes)
  // 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 向けに記述tool()description は AI がいつそのツールを選択するかを決定します。「何をするか、いつ使うか、何に使うべきでないか」を明確に記述してください。
  2. フィールドに .describe() を付ける:Zod フィールドには必ず .describe(...) を付けてください。AI はこの情報を使って呼び出しパラメータを構築します。
  3. 失敗には isError を使用し、例外をスローしない:AI に結果を見せてください。例外はモデルを困惑させ、リトライをトリガーする可能性があります。
  4. 読み取り専用 + readOnlyHint を優先:書き込み操作は慎重に行い、canUseTool または hooks で二次確認を行ってください。
  5. サーバー名は短く:ツールプレフィックスに含まれるため、長い名前はトークンを浪費します。
  6. In-process の共有状態はモジュールスコープに配置:handler はクロージャですが、各 query は同じ server インスタンスを再利用します。
  7. OAuth は最初の streamInput 前に完了mcpAuthenticate + mcpSubmitOAuthCallbackUrl を使用してください。セッション途中での認証完了は必然的にプロンプトプレフィックスキャッシュを破壊します。
  8. MCP 状態は mcpServerStatus() でプル:push チャネルは廃止されています。必要に応じてポーリングしてください。
  9. 適切な controlRequestTimeoutMs を設定:リモートサーバーのハンドシェイクは秒単位になる可能性があり、デフォルトの 60 秒で通常は十分ですが、CI 環境では明示的に指定してください。
  10. 分離には strictMcpConfig を使用:ユーザーローカルの 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?.();