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 Agent 调用外部工具的开放协议。通过 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 负责连接、发现、调用。
三种接入方式
| 方式 | 配置项 type | 进程边界 | 适用场景 |
|---|
| In-Process | 'sdk'(由 createSdkMcpServer 创建) | 同进程 | 自定义业务工具,需要直接访问 host 状态 |
| Stdio | 'stdio'(可省略) | 子进程 | 已有 MCP 工具包(@modelcontextprotocol/server-*) |
| SSE / HTTP | 'sse' / 'http' | 远程 | 远端服务、SaaS 工具、需要 OAuth 的服务 |
三种方式可以混用——在同一个 query() 里同时注册多个不同类型的服务器。
In-Process Server(推荐)
In-process 工具是最直接的扩展方式:定义一个普通的 async 函数,加上 Zod schema,就能被 Agent 调用。
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
};
| 参数 | 说明 |
|---|
name | 工具名,全限定名将是 mcp__<server>__<name> |
description | 给模型看的说明,决定 AI 何时调用——写清楚 What/When |
inputSchema | Zod raw shape(不是 z.object(...),传字段对象即可) |
handler | 实际逻辑,返回 CallToolResult |
extras.annotations | MCP 工具注解,详见下表 |
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 不会消费、也不会回传给宿主。如果你的应用需要这些信息,请在宿主侧自行维护映射。
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 知道「这个调用失败了,请换个办法」。
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。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 把列出的工具加入”自动放行”规则——调用时跳过权限弹窗,但不会把没列出来的工具藏起来。常用于让低风险的 MCP 工具免审批:
options: {
mcpServers: { my_tools: server },
allowedTools: [
'mcp__my_tools__greet', // pre-approved, no prompt
'mcp__my_tools__search_docs',
],
}
不传 allowedTools 仅意味着没有预授权规则——模型仍能看到/调用所有工具,只是写操作会按 permissionMode 走审批流程。完整语义详见 Permissions 文档。
allowedMcpServerNames:进程类 server 白名单
只过滤进程类(stdio/sse/http)服务器,不影响 in-process 服务器。配合 strictMcpConfig: true 可以拒绝 CLI 加载本地额外配置:
⚠️ 不传 allowedMcpServerNames 等于全部放开:所有声明的进程类 server 都会连接;想收口必须显式列出。in-process server 始终不受此字段影响。
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
}
运行时管理(Query API)
query() 返回的 Query 对象上挂了几把 MCP 钥匙。所有方法都通过 control channel 与 CLI 通信,行为是异步且幂等的。
⚠️ 缓存原则:MCP server 配置 / 鉴权状态变更会重建 tools 列表,会话中途变更会破坏 prompt prefix 缓存。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 可能要几百毫秒,建议用 pollUntil 等到 connected 再用。
订阅状态变化
MCP 状态采用拉取而非推送:调用 await q.mcpServerStatus() 即可。需要轮询时在自己代码里做。
改 server 集合?请走进程级配置
为保证 prompt prefix 缓存稳定,server 集合的变更在启动时一次完成:
| 你想做的事 | 怎么做 |
|---|
| 增加 / 删除 / 替换 servers | 在 options.mcpServers 里配置;需要变更集合时重启 query() |
| 仅启用部分进程类 server | options.allowedMcpServerNames 白名单 |
| 重连某个 server | 重启 query()(重连会重新发现 tools,影响缓存) |
| 退出某个 server 的登录 | 重启 query() 时不带该 token;或通过外部凭据存储清除后重启 |
控制请求超时
options: {
controlRequestTimeoutMs: 20_000, // default 60_000; pass 0 to disable
}
超时后 SDK 会自动写一条 control_cancel_request,并 reject 当前 Promise。
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:服务器请求用户输入。
宿主自己控制 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 默认把 token 存到系统 Keychain(macOS / Linux Secret Service),回退到 ~/.qoder/mcp-oauth-tokens.json(0o600 权限 + 跨进程锁)。
Elicitation:服务器请求用户输入
MCP elicitation/create 是 server → client 方向的请求,用来让 client 在用户面前展示一段交互。SDK 把这种请求通过 Options.onElicitation 暴露给宿主。
两种模式
| 模式 | 触发场景 | 典型用途 |
|---|
'form' | 服务器要一段结构化输入,请求带 requestedSchema(MCP 受限子集的 JSON Schema) | API key 录入、配置项填写、二次确认 |
'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
};
signal 在 q.close() / 中断时会 abort,长流程要检查。
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 消息。如果你 await 整个 OAuth 跳转,会触发 control 请求超时(controlRequestTimeoutMs)。
与 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 即可——发了就是服务器主导。
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 版本演进。
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 |
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 内部配置或外部状态决定) |
最佳实践
- 描述写给 AI 看:
tool() 的 description 决定 AI 何时选用它。说清楚「做什么、什么时候用、不该用于什么」。
- 字段
.describe():Zod 字段一定要带 .describe(...),AI 用这些信息构造调用参数。
- 失败用
isError,不抛异常:让 AI 看见结果。异常会让模型一脸懵,且可能触发重试。
- 优先只读 +
readOnlyHint:写操作要谨慎,搭配 canUseTool 或 hooks 二次确认。
- 服务器名简短:会出现在工具前缀里,太长的名字浪费 token。
- In-process 共享状态放模块作用域:handler 是闭包,但每次 query 仍会 reuse 同一个 server 实例。
- OAuth 在首次
streamInput 前完成:用 mcpAuthenticate + mcpSubmitOAuthCallbackUrl。会话中途完成鉴权必然破坏 prompt prefix 缓存。
- MCP 状态用
mcpServerStatus() 拉:push 通道已下线;按需轮询即可。
- 设置合理的
controlRequestTimeoutMs:远端服务器握手可能上秒,默认 60s 通常够,CI 环境记得显式给。
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?.();