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 ─▶ qoderclicn 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 负责连接、发现、调用。

三种接入方式

方式配置项 type进程边界适用场景
In-Process'sdk'(由 createSdkMcpServer / create_sdk_mcp_server 创建)同进程自定义业务工具,需要直接访问 host 状态
Stdio'stdio'(可省略)子进程已有 MCP 工具包(@modelcontextprotocol/server-*
SSE / HTTP'sse' / 'http'远程远端服务、SaaS 工具、需要 OAuth 的服务
三种方式可以混用——在同一个会话里同时注册多个不同类型的服务器。
💡 Python 中 mcp_servers 也可以传 str / pathlib.Path:指向一个 JSON 配置文件路径,SDK 会以 --mcp-config <path> 透传给 CLI。

In-Process Server(推荐)

In-process 工具是最直接的扩展方式:定义一个普通的 async 函数,加上 schema 声明,就能被 Agent 调用。详细的工具创建 / schema / handler 行为见 Tools,本节只覆盖与 MCP server 装配相关的部分。

30 秒上手

import { query, createSdkMcpServer, tool } from '@qodercn-ai/qodercn-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)server 名(决定工具前缀 mcp__<name>__
version默认 '1.0.0'
tools工具列表
返回值形如 { type: 'sdk', name, instance },直接塞进 MCP servers 配置即可。
⚠️ 不要复用同一个 server 配置跨多次 query():每次 query 会绑定独立的 transport。重复使用没有副作用,但你也不会得到「跨 query 共享状态」的能力——共享状态请放在 handler 闭包外的模块作用域里。

annotations 实际支持

下列三个字段会被 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声明工具会触达外部世界(如联网搜索、调用第三方 API)。TUI 工具详情会渲染 [open-world] 徽章annotations.openWorld
注意宿主侧字段名是去掉 Hint 后缀的:readOnlyHintannotations.readOnly,依此类推。annotations 对象只包含被显式设置的字段。 ⚠️ 这三个字段不会影响 auto 模式的权限决策。CLI 把 server 自声明的 annotation 视为不可验证的提示信息(server 可以随意 under-/over-declare),不会把它们带进权限管线,以免变相替 server 的自我标榜背书。要硬性拒绝某些工具,请用工具白名单或 hooks 拦截——annotation 仅用于宿主侧识别和 TUI 展示。
idempotentHinttitle 目前不被 SDK 消费——传了不会报错,但 SDK 不会消费、也不会回传给宿主。如果你的应用需要这些信息,请在宿主侧自行维护映射。
💡 关于 maxResultSizeChars:Python SDK 通过 ToolAnnotations(maxResultSizeChars=...)anthropic/maxResultSizeChars 写到工具的 _meta,CLI 据此放宽默认 50K 的返回长度限制(TS 通过同名 annotation 暴露,wire 一致)。

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

Handler 取消信号(Python)

Python handler 可以选择接收第二个参数 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 挂掉,server 状态非 'connected',其它 server 不受影响。需要 OAuth 的远端服务请看 OAuth 认证

工具命名与白名单

CLI 在向模型暴露 MCP 工具时统一加前缀:
mcp__<server_name>__<tool_name>
例如服务器名 my_tools、工具名 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 server 的工具都会暴露给模型。生产环境建议显式列出,按需收口。

预授权列表(不是可见性白名单)

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 回调,每次状态变化都会被调用一次;或消费消息流过滤 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
增加 / 删除 / 替换 serversoptions.mcpServers 里配置;需要变更集合时重启 query()启动时配置 mcp_servers;运行时可用 client.set_mcp_servers(servers) 全量替换(返回 {added, removed, errors}
仅启用部分进程类 serverallowedMcpServerNames 白名单allowed_mcp_server_names 白名单
重连某个 server重启 query()client.reconnect_mcp_server(name),常用于从 'failed' 状态恢复
启用 / 禁用某个 server重启 query()client.toggle_mcp_server(name, enabled);禁用会断开连接并下线其工具
退出某个 server 的登录重启 query() 时不带该 tokenclient.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 持久化。还有另一条服务器主导的鉴权链路——server 用 MCP elicitation/create 让 client 跳转去某个 URL 完成授权(典型例子:GitHub MCP)。两条链路独立,不会同时触发。详见 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 凭据,相当于「sign out」任意时刻;下次工具调用会触发重新鉴权
redirectUri / redirect_uri 可选,覆盖默认 OAuth 回调目标(Electron 自定义协议、企业内网回调地址等)。 CLI 默认把 token 存到系统 Keychain(macOS / Linux Secret Service),回退到 ~/.qoder-cn/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 流程,直接给 CLI 注入 token
{"callbackUrl": "..."}宿主只拿到完整的回调 URL(含 code / state),CLI 解析并换 token
{"code": "...", "state": "..."}宿主自己解出了 code,直接交还 CLI
None拒绝,CLI 把该 server 标为 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)暴露给宿主。

两种模式

模式触发场景典型用途
'form'服务器要一段结构化输入,请求带 requestedSchema(MCP 受限子集的 JSON Schema)API key 录入、配置项填写、二次确认
'url'服务器让用户去某个 URL 完成操作,请求带 url + elicitationId服务器自带 OAuth、设备码激活、账号关联
URL 模式异步完成:server 在自己的回调里收到用户授权后,会发 notifications/elicitation/complete——SDK 投影为 elicitation complete 消息推到消息流里。
⚠️ qoderclicn 当前在 MCP capability 声明里只送 elicitation: {}(等价于 { form: {} }),因此目前只有 form 模式真正能从远端 server 抵达 client。URL 模式协议层完整,但需要 CLI 显式声明 elicitation.url capability——后续随 CLI 版本演进,届时该路径会自动接通。

回调签名

import type { OnElicitation, ElicitationRequest, ElicitationResult } from '@qodercn-ai/qodercn-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 消息。如果你 await 整个 OAuth 跳转,会触发 control 请求超时。

与 OAuth 链路的边界

  • CLI 主导 OAuthmcpAuthenticate / mcp_authenticate 等):token 落 qoderclicn Keychain;MCP 状态在 needs-auth 时驱动;不触发 elicitation 回调。
  • 服务器主导 elicit URL:token 在 server 内部;MCP 状态不会标 needs-auth;靠 elicitation 回调接住、靠 elicitation_complete 消息收尾。
两条链路不互斥但也不重叠:同一个 server 通常只走其中一条。不知道某个 server 走哪条时:看它是否在握手时给客户端发 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 qodercn_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_configfalse禁止 CLI 从用户配置文件再加载额外 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 两种模式)
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}任意时刻(会破坏前缀缓存)
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 '@qodercn-ai/qodercn-agent-sdk';

import { tool, createSdkMcpServer } from '@qodercn-ai/qodercn-agent-sdk';
import type {
  AnyZodRawShape,
  InferShape,
  SdkMcpToolDefinition,
} from '@qodercn-ai/qodercn-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. 服务器名简短:会出现在工具前缀里,太长的名字浪费 token。
  6. In-process 共享状态放模块作用域:handler 是闭包,但每次 query 仍会 reuse 同一个 server 实例。
  7. OAuth 在首条用户消息前完成:会话中途完成鉴权必然破坏 prompt prefix 缓存。
  8. MCP 状态按需拉取:TypeScript 用 mcpServerStatus() 轮询;Python 可选 get_mcp_status()on_mcp_status_change 回调。
  9. 设置合理的 control 请求超时:远端服务器握手可能上秒,默认 60s 通常够;OAuth 等待用户操作时要调大;CI 环境记得显式给。
  10. strict MCP config 用于隔离:避免用户本地的 settings.json / .mcp.json 里声明的 MCP 服务器干扰你的应用。

完整示例

import { query, createSdkMcpServer, tool } from '@qodercn-ai/qodercn-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?.();