> ## 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 Integration

MCP (Model Context Protocol) is an open protocol for AI Agents to invoke external tools. The SDK lets you define MCP Servers and equip your Agent with tools; the underlying CLI handles connection management, tool discovery, and other runtime work.

<div id="architecture-overview" />

## Architecture Overview

```
┌────────────────────────────────────────────────────────────┐
│  Your Node.js application (SDK Host)                       │
│                                                            │
│   ┌──────────────────────────┐                             │
│   │ createSdkMcpServer(...)  │  ← In-Process tools         │
│   │  + tool(...)             │     defined inline, no extra process │
│   └──────────────────────────┘                             │
│                  │                                         │
│                  ▼                                         │
│   ┌──────────────────────────┐                             │
│   │  query({ mcpServers })   │── stdio ─▶ qodercli child  │
│   └──────────────────────────┘                             │
│                                          │                 │
│                                          ├── stdio ──▶ MCP server (process)
│                                          ├── sse   ──▶ MCP server (HTTP/SSE)
│                                          └── http  ──▶ MCP server (Streamable HTTP)
└────────────────────────────────────────────────────────────┘
```

* **In-Process**: The tool is a JS function running in your own process. The McpServer instance communicates with the CLI via the SDK's control channel without spawning an additional child process.
* **External**: You declare a child process or remote URL in the configuration; the CLI handles connection, discovery, and invocation.

***

<div id="three-integration-methods" />

## Three Integration Methods

| Method         | Config type                               | Process Boundary | Use Case                                                       |
| -------------- | ----------------------------------------- | ---------------- | -------------------------------------------------------------- |
| **In-Process** | `'sdk'` (created by `createSdkMcpServer`) | Same process     | Custom application tools that need direct access to host state |
| **Stdio**      | `'stdio'` (can be omitted)                | Child process    | Existing MCP toolkits (`@modelcontextprotocol/server-*`)       |
| **SSE / HTTP** | `'sse'` / `'http'`                        | Remote           | Remote services, SaaS tools, services requiring OAuth          |

All three methods can be **mixed** — register multiple servers of different types in the same `query()`.

***

<div id="in-process-server-recommended" />

## In-Process Server (Recommended)

In-process tools are the most straightforward extension method: define a regular async function, add a Zod schema, and it becomes callable by the Agent.

<div id="30-second-getting-started" />

### 30-Second Getting Started

```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-full-signature" />

### `tool()` Full Signature

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

| Parameter            | Description                                                                                                             |
| -------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| `name`               | Tool name; the fully-qualified name will be `mcp__<server>__<name>`                                                     |
| `description`        | Description for the model, determining when the AI invokes it — **clearly state what the tool does and when to use it** |
| `inputSchema`        | Zod raw shape (not `z.object(...)`, just pass the field object)                                                         |
| `handler`            | Actual logic, returns `CallToolResult`                                                                                  |
| `extras.annotations` | MCP tool annotations, see table below                                                                                   |

<div id="annotations-actually-consumed" />

#### Annotations Actually Consumed

The three fields below are consumed by the SDK and returned to the host via `mcpServerStatus().tools[i].annotations`:

| Field             | What it does                                                                                                                                                                    | Host-side reads as        |
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------- |
| `readOnlyHint`    | Declares the tool is **read-only**. Read-only tools may run concurrently (they don't block each other in the same batch); the TUI renders a `[read-only]` badge in tool details | `annotations.readOnly`    |
| `destructiveHint` | Declares the tool performs **destructive operations**. The TUI renders a `[destructive]` badge in tool details                                                                  | `annotations.destructive` |
| `openWorldHint`   | Declares the tool interacts with the **outside world** (e.g., web search, third-party API calls). The TUI renders an `[open-world]` badge in tool details                       | `annotations.openWorld`   |

> Note that host-side field names **drop the `Hint` suffix**: `readOnlyHint` → `annotations.readOnly`, and so on. The `annotations` object only contains fields that were explicitly set.
>
> ⚠️ **These fields do NOT affect auto-mode permission decisions**. CLI treats server-declared annotations as unverifiable advisory metadata (servers can freely under- or over-declare) and intentionally keeps them out of the permission pipeline — admitting them would launder authority for the server's self-assessment. **To hard-block specific tools, use the `allowedTools` allowlist or hooks** — annotations are for host-side identification (`mcpServerStatus`) and TUI display only.

`idempotentHint` and `title` are not currently supported — passing them won't error, but the SDK won't consume them or return them to the host. If your application needs this information, maintain the mapping yourself on the host side.

<div id="calltoolresult-structure" />

#### `CallToolResult` Structure

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

**Use `isError: true` for operational failures** instead of throwing exceptions — exceptions will terminate the entire tool call and the AI won't get information; `isError` lets the AI know "this call failed, please try another approach."

```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-full-signature" />

### `createSdkMcpServer()` Full Signature

```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;
```

The return value is shaped like `{ type: 'sdk', name, instance }` and can be directly placed into `options.mcpServers`.

> ⚠️ **Do not reuse the same server config across multiple `query()` calls**: Each query binds an independent transport. Reusing the same config has no side effects, but you won't get "cross-query shared state" capability either — for shared state, place it in module scope outside the handler closure.

<div id="multi-tool-example" />

### Multi-tool Example

```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

Communicates with MCP servers via a child process's stdin/stdout. The `@modelcontextprotocol/server-*` packages on NPM are all stdio implementations.

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

For remote services requiring OAuth, see [OAuth Authentication](#oauth-authentication).

***

<div id="tool-naming-and-allowlists" />

## Tool Naming and Allowlists

The CLI uniformly prefixes MCP tools when exposing them to the model:

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

For example, server name `my_tools` with tool name `greet` gives the model the tool name `mcp__my_tools__greet`.

<div id="tools-restrict-which-tools-the-model-can-see" />

### `tools`: Restrict Which Tools the Model Can See

Use `tools` when you want the model to **only see a subset of tools**. The CLI adds every built-in tool not in the list to the disallow set — effectively a visibility allowlist:

```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',
  ],
}
```

> ⚠️ **Omitting `tools` means everything is exposed**: all built-in tools plus every tool from connected MCP servers reach the model. For production, list them explicitly to tighten scope.

<div id="allowedtools-pre-approval-not-a-visibility-allowlist" />

### `allowedTools`: Pre-approval (**Not** a Visibility Allowlist)

`allowedTools` adds listed tools to the "always-allow" rule set — calls **skip the permission prompt**, but unlisted tools are **not** hidden. Use it to whitelist low-risk MCP tools for unattended use:

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

Omitting `allowedTools` just means no pre-approval rules — the model still sees and can call every tool, write operations simply route through the regular `permissionMode` approval flow. See [Permissions docs](/en/cli/sdk/permissions#controlling-tool-scope-tools-allowedtools-disallowedtools) for full semantics.

<div id="allowedmcpservernames-process-server-allowlist" />

### `allowedMcpServerNames`: Process-Server Allowlist

Only filters **process-based** (stdio/sse/http) servers; **does not affect in-process servers**. Combined with `strictMcpConfig: true`, it can prevent the CLI from loading additional local configurations:

```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
}
```

> ⚠️ **Omitting `allowedMcpServerNames` means all process servers connect**; to tighten, list them explicitly. In-process servers are never filtered by this field.

***

<div id="runtime-management-query-api" />

## Runtime Management (Query API)

The `Query` object returned by `query()` exposes several MCP-related methods. All methods communicate with the CLI via the control channel and are asynchronous and idempotent.

> ⚠️ **Caching Principle**: MCP server config/auth state changes rebuild the tools list, which **breaks the prompt prefix cache mid-session**. The SDK provides methods for "querying status + completing auth before the first message"; the server set itself should be configured once at startup via `options.mcpServers`, restarting `query()` when necessary.

<div id="querying-status" />

### Querying Status

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

> 💡 The MCP handshake occurs after the CLI completes `initialize` but before the first user message. Query status only after `initializationResult()` has returned to get real results — handshake IO may take a few hundred milliseconds; consider using `pollUntil` to wait for `connected` before proceeding.

<div id="subscribing-to-status-changes" />

### Subscribing to Status Changes

MCP status uses **pull** rather than push: call `await q.mcpServerStatus()`. Implement polling in your own code when needed.

<div id="changing-the-server-set-use-process-level-configuration" />

### Changing the Server Set? Use Process-level Configuration

To keep the prompt prefix cache stable, server set changes are completed once at startup:

| What You Want to Do                    | How to Do It                                                                              |
| -------------------------------------- | ----------------------------------------------------------------------------------------- |
| Add / remove / replace servers         | Configure in `options.mcpServers`; restart `query()` when the set needs to change         |
| Enable only some process-based servers | `options.allowedMcpServerNames` allowlist                                                 |
| Reconnect a server                     | Restart `query()` (reconnection rediscovers tools, affecting cache)                       |
| Log out of a server                    | Restart `query()` without that token; or clear via external credential store then restart |

<div id="controlling-request-timeout" />

### Controlling Request Timeout

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

After timeout, the SDK automatically writes a `control_cancel_request` and rejects the current Promise.

***

<div id="oauth-authentication" />

## OAuth Authentication

Remote MCP servers (HTTP/SSE) often require OAuth. The CLI has a complete built-in OAuth 2.0 + PKCE + Dynamic Client Registration (RFC 7591) implementation.

> ⚠️ **Caching Principle**: After OAuth completes, the CLI reconnects to the server and rediscovers tools, which **inevitably breaks the prompt prefix cache mid-session**. Therefore, only "actively-driven" auth mode is supported — **complete all auth before the first `streamInput`** so the tools list stabilizes before sending the first user message.

> 💡 **This section only covers CLI-driven OAuth**: The CLI performs metadata discovery, PKCE, token exchange, and token persistence. There is another **server-driven** auth path — where the server uses MCP `elicitation/create` to have the client redirect to a URL for authorization (typical example: GitHub MCP). The two paths are independent and won't trigger simultaneously: with server-driven auth, `mcpServerStatus()` won't show `needs-auth`, and `mcpAuthenticate` shouldn't be called; instead, the host uses `onElicitation` to handle the request. See [Elicitation: Server Requests User Input](#elicitation-server-requests-user-input).

The host controls OAuth timing, completing it **before** sending the first user message:

```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 Method                            | Purpose                                                                                                                              | When to Call                       |
| -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------- |
| `mcpAuthenticate(name, redirectUri?)`  | Initiate OAuth; returns `{ authUrl?, requiresUserAction }`. When silent renewal succeeds, `requiresUserAction: false` — no UI needed | **Before the first `streamInput`** |
| `mcpSubmitOAuthCallbackUrl(name, url)` | Submit the complete callback URL (with code/state)                                                                                   | **Before the first `streamInput`** |

`redirectUri` is optional, overriding the default OAuth callback target (Electron custom protocol, enterprise intranet callback addresses, etc.).

The CLI stores tokens in the system Keychain by default (macOS / Linux Secret Service), falling back to `~/.qoder/mcp-oauth-tokens.json` (0o600 permissions + cross-process locking).

***

<div id="elicitation-server-requests-user-input" />

## Elicitation: Server Requests User Input

MCP `elicitation/create` is a **server → client** request used to have the client display an interaction to the user. The SDK exposes these requests to the host via `Options.onElicitation`.

<div id="two-modes" />

### Two Modes

| Mode     | Trigger Scenario                                                                                           | Typical Use                                                |
| -------- | ---------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------- |
| `'form'` | Server requests structured input; request carries `requestedSchema` (MCP restricted subset of JSON Schema) | API key entry, configuration input, secondary confirmation |
| `'url'`  | Server asks user to visit a URL to complete an action; request carries `url` + `elicitationId`             | Server-side OAuth, device code activation, account linking |

URL mode completes asynchronously: after the server receives user authorization in its own callback, it sends `notifications/elicitation/complete` — the SDK projects this as an `SDKElicitationCompleteMessage` pushed into the `Query` message stream.

<div id="callback-signature" />

### Callback Signature

```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` will abort on `q.close()` / interrupt; long-running processes should check it.

<div id="form-mode-example" />

### Form Mode Example

```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-mode-example-with-elicitation-complete" />

### URL Mode Example (with 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.
  }
}
```

> 💡 **Do not await the browser redirect inside `onElicitation`**. The URL mode design is: the callback immediately returns `accept` (= user has started the flow), and the CLI does not block the control channel; the real "completion" signal comes from the subsequent `elicitation_complete` message. If you await the entire OAuth redirect, you'll trigger the control request timeout (`controlRequestTimeoutMs`).

<div id="boundary-with-the-oauth-path" />

### Boundary with the OAuth Path

* **CLI-driven OAuth** (`mcpAuthenticate` / `mcpSubmitOAuthCallbackUrl`): Token stored in qodercli Keychain; driven when `mcpServerStatus()` shows `needs-auth`; **does NOT trigger** `onElicitation`.
* **Server-driven elicit URL**: Token stays internal to the server; `mcpServerStatus()` won't show `needs-auth`; **handled via `onElicitation`**; completed via `system/elicitation_complete`.

The two paths are not mutually exclusive but don't overlap: the same server typically uses only one. If unsure which path a server uses: check whether it sends `elicitation/create` to the client during handshake — if it does, it's server-driven.

<div id="hook-channel" />

### Hook Channel

Hosts can also attach hooks in `settings.json` to intercept elicitation, with behavior taking priority over `onElicitation`:

| Hook Event                                   | Timing                                              | Capability                                                               |
| -------------------------------------------- | --------------------------------------------------- | ------------------------------------------------------------------------ |
| `Elicitation`                                | When server request arrives, before `onElicitation` | Auto `accept` / `decline` / `cancel` (short-circuit UI), or pass through |
| `ElicitationResult`                          | After user responds                                 | Rewrite `action` / `content`, or block (force decline)                   |
| `Notification` (type=`elicitation_complete`) | When URL mode completion notification arrives       | Trigger IDE / system notification                                        |

> ⚠️ qodercli 0.2.x only sends `elicitation: {}` (empty object, compatible with Spring AI Java MCP SDK) in the MCP capability declaration. The MCP SDK server-side interprets this as equivalent to `{ form: {} }`, so **currently only form mode actually arrives at the client from remote servers**. The URL mode protocol layer is complete, but requires the CLI to explicitly declare `elicitation.url` for the server-side `elicitInput({ mode: 'url' })` to pass validation — this will evolve with CLI version updates.

***

<div id="options-reference" />

## Options Reference

| Field                     | Type                              | Default  | Description                                                                                                                              |
| ------------------------- | --------------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| `mcpServers`              | `Record<string, McpServerConfig>` | –        | Server name → config                                                                                                                     |
| `allowedMcpServerNames`   | `string[]`                        | –        | Process-based server allowlist (does not affect in-process); omitting means all are open                                                 |
| `strictMcpConfig`         | `boolean`                         | `false`  | Prevent CLI from loading additional MCP from user config files                                                                           |
| `tools`                   | `string[]`                        | –        | **Model-visible tool allowlist**; omitting means every built-in + MCP tool is visible                                                    |
| `allowedTools`            | `string[]`                        | –        | **Pre-approval** list (skip permission prompts; **does not** control visibility); omitting means no pre-approval rules                   |
| `disallowedTools`         | `string[]`                        | –        | Explicit deny list; takes precedence over allow                                                                                          |
| `controlRequestTimeoutMs` | `number`                          | `60_000` | Control request timeout (including mcp series), 0 to disable                                                                             |
| `onElicitation`           | `OnElicitation`                   | –        | Triggered when an MCP server actively requests user input (form / url modes), see [Elicitation](#elicitation-server-requests-user-input) |

<div id="methods-on-query" />

### Methods on Query

| Method                                 | Description                                                         | When to Call                       |
| -------------------------------------- | ------------------------------------------------------------------- | ---------------------------------- |
| `mcpServerStatus()`                    | Get current status of all MCP servers                               | Any time                           |
| `mcpAuthenticate(name, redirectUri?)`  | Actively initiate OAuth; returns `{ authUrl?, requiresUserAction }` | **Before the first `streamInput`** |
| `mcpSubmitOAuthCallbackUrl(name, url)` | Submit OAuth callback                                               | **Before the first `streamInput`** |

> For adding/removing/modifying the server set, use `options.mcpServers` (configured at startup) + restart `query()`; see [Changing the Server Set? Use Process-level Configuration](#changing-the-server-set-use-process-level-configuration).

***

<div id="type-reference" />

## Type Reference

```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` enum:

| Value          | Meaning                                                        |
| -------------- | -------------------------------------------------------------- |
| `'pending'`    | Registered, connection not yet started                         |
| `'connecting'` | Handshaking                                                    |
| `'connected'`  | Connected, tools are callable                                  |
| `'failed'`     | Connection failed (check the `error` field)                    |
| `'needs-auth'` | Requires OAuth, proceed with auth flow                         |
| `'disabled'`   | Disabled (determined by CLI internal config or external state) |

***

<div id="best-practices" />

## Best Practices

1. **Write descriptions for the AI**: The `tool()` `description` determines when the AI selects it. Clearly state "what it does, when to use it, what it should NOT be used for."
2. **Use `.describe()` on fields**: Always add `.describe(...)` to Zod fields; the AI uses this information to construct call parameters.
3. **Use `isError` for failures, don't throw exceptions**: Let the AI see the result. Exceptions confuse the model and may trigger retries.
4. **Prefer read-only + `readOnlyHint`**: Be cautious with write operations; pair with `canUseTool` or hooks for secondary confirmation.
5. **Keep server names short**: They appear in tool prefixes; overly long names waste tokens.
6. **Place in-process shared state in module scope**: Handlers are closures, but each query still reuses the same server instance.
7. **Complete OAuth before the first `streamInput`**: Use `mcpAuthenticate` + `mcpSubmitOAuthCallbackUrl`. Completing auth mid-session inevitably breaks the prompt prefix cache.
8. **Pull MCP status with `mcpServerStatus()`**: The push channel has been retired; poll as needed.
9. **Set a reasonable `controlRequestTimeoutMs`**: Remote server handshakes may take seconds; the default 60s is usually sufficient, but set it explicitly in CI environments.
10. **Use `strictMcpConfig` for isolation**: Prevent MCP servers declared in the user's local `settings.json` / `.mcp.json` from interfering with your application.

***

<div id="complete-example" />

## Complete Example

```typescript theme={null}
import { query, createSdkMcpServer, tool } from '@qoder-ai/qoder-agent-sdk';
import { z } from 'zod';

// 1. Define application 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?.();
```
