> ## 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 集成

MCP（Model Context Protocol）是 AI Agent 调用外部工具的开放协议。通过 SDK，你可以定义 MCP Server、为 Agent 配置工具。连接管理、工具发现等运行时工作由底层 CLI 完成。

<div id="一图看懂" />

## 一图看懂

```
┌────────────────────────────────────────────────────────────┐
│  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 负责连接、发现、调用。

***

<div id="三种接入方式" />

## 三种接入方式

| 方式             | 配置项 type                           | 进程边界 | 适用场景                                         |
| -------------- | ---------------------------------- | ---- | -------------------------------------------- |
| **In-Process** | `'sdk'`（由 `createSdkMcpServer` 创建） | 同进程  | 自定义业务工具，需要直接访问 host 状态                       |
| **Stdio**      | `'stdio'`（可省略）                     | 子进程  | 已有 MCP 工具包（`@modelcontextprotocol/server-*`） |
| **SSE / HTTP** | `'sse'` / `'http'`                 | 远程   | 远端服务、SaaS 工具、需要 OAuth 的服务                    |

三种方式可以**混用**——在同一个 `query()` 里同时注册多个不同类型的服务器。

***

<div id="in-process-server推荐" />

## In-Process Server（推荐）

In-process 工具是最直接的扩展方式：定义一个普通的 async 函数，加上 Zod schema，就能被 Agent 调用。

<div id="30-秒上手" />

### 30 秒上手

```typescript theme={null}
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);
}
```

<div id="tool-完整签名" />

### `tool()` 完整签名

```typescript theme={null}
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**     |
| `inputSchema`        | Zod raw shape（不是 `z.object(...)`，传字段对象即可） |
| `handler`            | 实际逻辑，返回 `CallToolResult`                  |
| `extras.annotations` | MCP 工具注解，详见下表                             |

<div id="annotations-实际支持" />

#### annotations 实际支持

下列三个字段会被 SDK 真正消费,并通过 `mcpServerStatus().tools[i].annotations` 回传到宿主侧:

| 字段                | 作用                                                              | 宿主侧读法                     |
| ----------------- | --------------------------------------------------------------- | ------------------------- |
| `readOnlyHint`    | 声明工具是**只读**的。只读工具可以并发执行(同批次内不互相阻塞);TUI 工具详情会渲染 `[read-only]` 徽章 | `annotations.readOnly`    |
| `destructiveHint` | 声明工具会执行**破坏性操作**。TUI 工具详情会渲染 `[destructive]` 徽章                 | `annotations.destructive` |
| `openWorldHint`   | 声明工具会触达**外部世界**(如联网搜索、调用第三方 API)。TUI 工具详情会渲染 `[open-world]` 徽章  | `annotations.openWorld`   |

> 注意宿主侧字段名是**去掉 `Hint` 后缀**的:`readOnlyHint` → `annotations.readOnly`,依此类推。`annotations` 对象只包含被显式设置的字段。
>
> ⚠️ **这三个字段不会影响 auto 模式的权限决策**。CLI 把 server 自声明的 annotation 视为不可验证的提示信息(server 可以随意 under-/over-declare),不会把它们带进权限管线,以免变相替 server 的自我标榜背书。**要硬性拒绝某些工具,请用 `allowedTools` 白名单或 hooks 拦截**——annotation 仅用于宿主侧识别(`mcpServerStatus`)和 TUI 展示。

`idempotentHint` 和 `title` 目前不支持——传了不会报错,但 SDK 不会消费、也不会回传给宿主。如果你的应用需要这些信息,请在宿主侧自行维护映射。

<div id="calltoolresult-结构" />

#### `CallToolResult` 结构

```typescript theme={null}
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`** 而不是抛异常——异常会终止整个 tool call，AI 拿不到信息；`isError` 让 AI 知道「这个调用失败了，请换个办法」。

```typescript theme={null}
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 } },
);
```

<div id="createsdkmcpserver-完整签名" />

### `createSdkMcpServer()` 完整签名

```typescript theme={null}
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 闭包外的模块作用域里。

<div id="多工具示例" />

### 多工具示例

```typescript theme={null}
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 /* ... */],
});
```

***

<div id="stdio-server" />

## Stdio Server

通过子进程的 stdin/stdout 与 MCP 服务器通信。NPM 上 `@modelcontextprotocol/server-*` 系列都是 stdio 实现。

```typescript theme={null}
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)
};
```

```typescript theme={null}
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! },
      },
    },
  },
});
```

***

<div id="sse-http-server" />

## SSE / HTTP Server

```typescript theme={null}
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;
};
```

```typescript theme={null}
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 认证](#oauth-认证)。

***

<div id="工具命名与白名单" />

## 工具命名与白名单

CLI 在向模型暴露 MCP 工具时统一加前缀：

```
mcp__<server_name>__<tool_name>
```

例如服务器名 `my_tools`、工具名 `greet`，模型看到的工具名是 `mcp__my_tools__greet`。

<div id="tools限制模型可见的工具集合" />

### `tools`：限制模型可见的工具集合

想让模型**只看到部分工具**，用 `tools`。CLI 会把所有未列出的内置工具加进 disallow 列表，等于"白名单"语义：

```typescript theme={null}
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 server 的工具都会暴露给模型。生产环境建议显式列出，按需收口。

<div id="allowedtools预授权不是可见性白名单" />

### `allowedTools`：预授权（**不是**可见性白名单）

`allowedTools` 把列出的工具加入"自动放行"规则——调用时**跳过权限弹窗**，但**不会**把没列出来的工具藏起来。常用于让低风险的 MCP 工具免审批：

```typescript theme={null}
options: {
  mcpServers: { my_tools: server },
  allowedTools: [
    'mcp__my_tools__greet',          // pre-approved, no prompt
    'mcp__my_tools__search_docs',
  ],
}
```

不传 `allowedTools` 仅意味着没有预授权规则——模型仍能看到/调用所有工具，只是写操作会按 `permissionMode` 走审批流程。完整语义详见 [Permissions 文档](/zh/cli/sdk/permissions#控制工具范围toolsallowedtoolsdisallowedtools)。

<div id="allowedmcpservernames进程类-server-白名单" />

### `allowedMcpServerNames`：进程类 server 白名单

只过滤**进程类**（stdio/sse/http）服务器，**不影响 in-process 服务器**。配合 `strictMcpConfig: true` 可以拒绝 CLI 加载本地额外配置：

> ⚠️ **不传 `allowedMcpServerNames` 等于全部放开**：所有声明的进程类 server 都会连接；想收口必须显式列出。in-process server 始终不受此字段影响。

```typescript theme={null}
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
}
```

***

<div id="运行时管理query-api" />

## 运行时管理（Query API）

`query()` 返回的 `Query` 对象上挂了几把 MCP 钥匙。所有方法都通过 control channel 与 CLI 通信，行为是异步且幂等的。

> ⚠️ **缓存原则**：MCP server 配置 / 鉴权状态变更会重建 tools 列表，**会话中途变更会破坏 prompt prefix 缓存**。SDK 提供"查询状态 + 首条消息前完成鉴权"的方法；server 集合本身请通过 `options.mcpServers` 在启动时一次性配置，必要时重启 `query()`。

<div id="查询状态" />

### 查询状态

```typescript theme={null}
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 可能要几百毫秒，建议用 `pollUntil` 等到 `connected` 再用。

<div id="订阅状态变化" />

### 订阅状态变化

MCP 状态采用**拉取**而非推送：调用 `await q.mcpServerStatus()` 即可。需要轮询时在自己代码里做。

<div id="改-server-集合请走进程级配置" />

### 改 server 集合？请走进程级配置

为保证 prompt prefix 缓存稳定，server 集合的变更在启动时一次完成：

| 你想做的事                | 怎么做                                            |
| -------------------- | ---------------------------------------------- |
| 增加 / 删除 / 替换 servers | 在 `options.mcpServers` 里配置；需要变更集合时重启 `query()` |
| 仅启用部分进程类 server      | `options.allowedMcpServerNames` 白名单            |
| 重连某个 server          | 重启 `query()`（重连会重新发现 tools，影响缓存）               |
| 退出某个 server 的登录      | 重启 `query()` 时不带该 token；或通过外部凭据存储清除后重启         |

<div id="控制请求超时" />

### 控制请求超时

```typescript theme={null}
options: {
  controlRequestTimeoutMs: 20_000,  // default 60_000; pass 0 to disable
}
```

超时后 SDK 会自动写一条 `control_cancel_request`，并 reject 当前 Promise。

***

<div id="oauth-认证" />

## OAuth 认证

远端 MCP 服务器（HTTP/SSE）经常需要 OAuth。CLI 内置完整的 OAuth 2.0 + PKCE + Dynamic Client Registration（RFC 7591）实现。

> ⚠️ **缓存原则**：OAuth 完成后 CLI 会重连 server、重新发现 tools，**会话中途完成鉴权必然破坏 prompt prefix 缓存**。所以只支持"主动驱动"模式鉴权——**在首次 `streamInput` 之前**完成所有鉴权，tools 列表稳定下来后再发首条用户消息。

> 💡 **本节只覆盖 CLI 主导的 OAuth**：CLI 自己做 metadata discovery、PKCE、token 交换、token 持久化。还有另一条**服务器主导**的鉴权链路——server 用 MCP `elicitation/create` 让 client 跳转去某个 URL 完成授权（典型例子：GitHub MCP）。两条链路独立，不会同时触发：服务器主导时 `mcpServerStatus()` 不会标 `needs-auth`、`mcpAuthenticate` 不该调，host 改用 `onElicitation` 接住请求。详见 [Elicitation:服务器请求用户输入](#elicitation服务器请求用户输入)。

宿主自己控制 OAuth 时机，在发首条用户消息**之前**完成：

```typescript theme={null}
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 默认把 token 存到系统 Keychain（macOS / Linux Secret Service），回退到 `~/.qoder/mcp-oauth-tokens.json`（0o600 权限 + 跨进程锁）。

***

<div id="elicitation服务器请求用户输入" />

## Elicitation:服务器请求用户输入

MCP `elicitation/create` 是 **server → client** 方向的请求,用来让 client 在用户面前展示一段交互。SDK 把这种请求通过 `Options.onElicitation` 暴露给宿主。

<div id="两种模式" />

### 两种模式

| 模式       | 触发场景                                                     | 典型用途                   |
| -------- | -------------------------------------------------------- | ---------------------- |
| `'form'` | 服务器要一段结构化输入,请求带 `requestedSchema`(MCP 受限子集的 JSON Schema) | API key 录入、配置项填写、二次确认  |
| `'url'`  | 服务器让用户去某个 URL 完成操作,请求带 `url` + `elicitationId`           | 服务器自带 OAuth、设备码激活、账号关联 |

URL 模式异步完成:server 在自己的回调里收到用户授权后,会发 `notifications/elicitation/complete` — SDK 投影为 `SDKElicitationCompleteMessage` 推到 `Query` 的消息流里。

<div id="回调签名" />

### 回调签名

```typescript theme={null}
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
};
```

`signal` 在 `q.close()` / 中断时会 abort,长流程要检查。

<div id="form-模式示例" />

### form 模式示例

```typescript theme={null}
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' };
    },
  },
});
```

<div id="url-模式示例配合-elicitation-complete" />

### url 模式示例(配合 elicitation\_complete)

```typescript theme={null}
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` 消息。如果你 await 整个 OAuth 跳转,会触发 control 请求超时(`controlRequestTimeoutMs`)。

<div id="与-oauth-链路的边界" />

### 与 OAuth 链路的边界

* **CLI 主导 OAuth**(`mcpAuthenticate` / `mcpSubmitOAuthCallbackUrl`):token 落 qodercli Keychain;`mcpServerStatus()` 在 `needs-auth` 时驱动;**不触发** `onElicitation`。
* **服务器主导 elicit URL**:token 在 server 内部;`mcpServerStatus()` 不会标 `needs-auth`;**靠 `onElicitation`** 接住、靠 `system/elicitation_complete` 收尾。

两条链路不互斥但也不重叠:同一个 server 通常只走其中一条。不知道某个 server 走哪条时:看它是否在握手时给客户端发 `elicitation/create` 即可——发了就是服务器主导。

<div id="hook-通道" />

### Hook 通道

宿主同样可以在 `settings.json` 里挂 hooks 拦截 elicitation,行为优先于 `onElicitation`:

| Hook 事件                                      | 时机                                | 能做什么                                          |
| -------------------------------------------- | --------------------------------- | --------------------------------------------- |
| `Elicitation`                                | server 请求到达时,在 `onElicitation` 之前 | 自动 `accept` / `decline` / `cancel`(短路 UI),或放行 |
| `ElicitationResult`                          | 用户响应之后                            | 改写 `action` / `content`,或 block(强制 decline)   |
| `Notification` (type=`elicitation_complete`) | URL 模式完成通知到达时                     | 触发 IDE / 系统通知                                 |

> ⚠️ qodercli 0.2.x 在 MCP capability 声明里只送 `elicitation: {}`(空对象,兼容 Spring AI Java MCP SDK)。MCP SDK 服务端会把这个等价解释为 `{ form: {} }`,因此**目前只有 form 模式真正能从远端 server 抵达 client**。URL 模式协议层完整,但需要 CLI 显式声明 `elicitation.url` 才能让 server 端的 `elicitInput({ mode: 'url' })` 通过校验——后续随 CLI 版本演进。

***

<div id="options-速查" />

## Options 速查

| 字段                        | 类型                                | 默认       | 说明                                                                              |
| ------------------------- | --------------------------------- | -------- | ------------------------------------------------------------------------------- |
| `mcpServers`              | `Record<string, McpServerConfig>` | –        | 服务器名 → 配置                                                                       |
| `allowedMcpServerNames`   | `string[]`                        | –        | 进程类服务器白名单（不影响 in-process）；不传等于全部放开                                              |
| `strictMcpConfig`         | `boolean`                         | `false`  | 禁止 CLI 从用户配置文件再加载额外 MCP                                                         |
| `tools`                   | `string[]`                        | –        | **模型可见工具白名单**；不传等于全部内置 + MCP 工具都可见                                              |
| `allowedTools`            | `string[]`                        | –        | **预授权**列表（跳过权限弹窗，**不**控制可见性）；不传等于无预授权规则                                         |
| `disallowedTools`         | `string[]`                        | –        | 明确拒绝的工具，优先于 allow                                                               |
| `controlRequestTimeoutMs` | `number`                          | `60_000` | control 请求超时（含 mcp 系列），0 禁用                                                     |
| `onElicitation`           | `OnElicitation`                   | –        | MCP server 主动请求用户输入时触发(form / url 两种模式),详见 [Elicitation](#elicitation服务器请求用户输入) |

<div id="query-上的方法" />

### Query 上的方法

| 方法                                     | 说明                                               | 调用时机                   |
| -------------------------------------- | ------------------------------------------------ | ---------------------- |
| `mcpServerStatus()`                    | 拿当前所有 MCP 服务器状态                                  | 任意时刻                   |
| `mcpAuthenticate(name, redirectUri?)`  | 主动启动 OAuth；返回 `{ authUrl?, requiresUserAction }` | **首次 `streamInput` 前** |
| `mcpSubmitOAuthCallbackUrl(name, url)` | 提交 OAuth 回调                                      | **首次 `streamInput` 前** |

> server 集合的增删改请通过 `options.mcpServers`（启动时配置）+ 重启 `query()` 完成，详见 [改 server 集合？请走进程级配置](#改-server-集合请走进程级配置)。

***

<div id="类型参考" />

## 类型参考

```typescript theme={null}
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 内部配置或外部状态决定） |

***

<div id="最佳实践" />

## 最佳实践

1. **描述写给 AI 看**：`tool()` 的 `description` 决定 AI 何时选用它。说清楚「做什么、什么时候用、不该用于什么」。
2. **字段 `.describe()`**：Zod 字段一定要带 `.describe(...)`，AI 用这些信息构造调用参数。
3. **失败用 `isError`，不抛异常**：让 AI 看见结果。异常会让模型一脸懵，且可能触发重试。
4. **优先只读 + `readOnlyHint`**：写操作要谨慎，搭配 `canUseTool` 或 hooks 二次确认。
5. **服务器名简短**：会出现在工具前缀里，太长的名字浪费 token。
6. **In-process 共享状态放模块作用域**：handler 是闭包，但每次 query 仍会 reuse 同一个 server 实例。
7. **OAuth 在首次 `streamInput` 前完成**：用 `mcpAuthenticate` + `mcpSubmitOAuthCallbackUrl`。会话中途完成鉴权必然破坏 prompt prefix 缓存。
8. **MCP 状态用 `mcpServerStatus()` 拉**：push 通道已下线；按需轮询即可。
9. **设置合理的 `controlRequestTimeoutMs`**：远端服务器握手可能上秒，默认 60s 通常够，CI 环境记得显式给。
10. **`strictMcpConfig` 用于隔离**：避免用户本地的 `settings.json` / `.mcp.json` 里声明的 MCP 服务器干扰你的应用。

***

<div id="完整示例" />

## 完整示例

```typescript theme={null}
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?.();
```
