Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
86 commits
Select commit Hold shift + click to select a range
96d88eb
feat(mcp): add 2026 protocol negotiation
samuelhsin Aug 12, 2026
707c2bc
feat(mcp): render MCP Apps in WebShell
samuelhsin Aug 12, 2026
b7d5212
Merge remote-tracking branch 'upstream/main' into review-pr-8992
samuelhsin Aug 12, 2026
609ffce
fix(mcp): keep legacy tool discovery lenient
samuelhsin Aug 12, 2026
1ed9888
fix(mcp): keep Apps HTML out of TUI and honor tool visibility
samuelhsin Aug 13, 2026
96b3b2b
fix(mcp): stabilize AppBridge lifetime and close sandbox CSP gaps
samuelhsin Aug 13, 2026
ffe40b6
Merge branch 'main' into codex/feat-mcp-2026-core
samuelhsin Aug 13, 2026
aa26beb
fix(mcp): list under-declared modern MCP capabilities over the wire
samuelhsin Aug 13, 2026
a901579
Merge branch 'main' into codex/feat-mcp-2026-core
samuelhsin Aug 13, 2026
e0f5b4d
Merge branch 'main' into codex/feat-mcp-2026-core
samuelhsin Aug 13, 2026
8c6e5d4
fix(mcp): keep Apps sandbox reachable and list past 64 pages
samuelhsin Aug 14, 2026
0cbeaa7
Merge upstream main into codex/feat-mcp-2026-core
cursoragent Aug 14, 2026
f067172
fix(mcp): reject empty compacted html and keep MCP Apps expanded in m…
cursoragent Aug 14, 2026
ab2eebc
feat(web-shell): fold thinking into the compact-mode tool summary (#9…
ytahdn Aug 14, 2026
652e20d
fix(mcp): address app discovery and sandbox regressions
samuelhsin Aug 14, 2026
2949420
fix(web-shell): keep MCP apps expanded in compact summaries
samuelhsin Aug 14, 2026
d4dff0e
Revert "feat(web-shell): fold thinking into the compact-mode tool sum…
samuelhsin Aug 14, 2026
994a7af
Merge branch 'main' into codex/feat-mcp-2026-core
samuelhsin Aug 15, 2026
67775f7
fix(web-shell): render compacted MCP App fallback and teardown before…
samuelhsin Aug 15, 2026
564b002
Merge branch 'main' into codex/feat-mcp-2026-core
samuelhsin Aug 15, 2026
6b14550
Merge branch 'main' into codex/feat-mcp-2026-core
samuelhsin Aug 15, 2026
ad07929
fix(mcp): bound the discover probe and raise the daemon bundle cap
samuelhsin Aug 15, 2026
23b95b7
Merge branch 'main' into codex/feat-mcp-2026-core
samuelhsin Aug 15, 2026
9f83263
Merge branch 'main' into codex/feat-mcp-2026-core
samuelhsin Aug 16, 2026
8ab746c
Merge branch 'main' into codex/feat-mcp-2026-core
samuelhsin Aug 16, 2026
aeb03ff
Merge branch 'main' into codex/feat-mcp-2026-core
samuelhsin Aug 16, 2026
c617d36
Merge branch 'main' into codex/feat-mcp-2026-core
samuelhsin Aug 16, 2026
fb0f9e3
Merge branch 'main' into codex/feat-mcp-2026-core
samuelhsin Aug 17, 2026
00585a9
Merge branch 'main' into codex/feat-mcp-2026-core
samuelhsin Aug 17, 2026
a084b28
fix(mcp): skip version-negotiation probe on remote transports
samuelhsin Aug 17, 2026
4b8860e
Merge branch 'main' into codex/feat-mcp-2026-core
samuelhsin Aug 17, 2026
c086aa7
Merge branch 'main' into codex/feat-mcp-2026-core
samuelhsin Aug 17, 2026
2069d6d
fix(web-shell): keep MCP App iframe src across deferred teardown
samuelhsin Aug 17, 2026
183db6a
Merge branch 'main' into codex/feat-mcp-2026-core
samuelhsin Aug 17, 2026
2ebc314
Merge branch 'main' into codex/feat-mcp-2026-core
samuelhsin Aug 17, 2026
6e282ff
fix(mcp): honor listing-level MCP App CSP and permissions
samuelhsin Aug 18, 2026
ad6a717
Merge branch 'main' into codex/feat-mcp-2026-core
samuelhsin Aug 18, 2026
5c83e3e
Merge branch 'main' into codex/feat-mcp-2026-core
samuelhsin Aug 18, 2026
1afc618
Merge branch 'main' into codex/feat-mcp-2026-core
samuelhsin Aug 18, 2026
4d56e8f
Merge branch 'main' into codex/feat-mcp-2026-core
samuelhsin Aug 18, 2026
51833f4
Merge branch 'main' into codex/feat-mcp-2026-core
samuelhsin Aug 18, 2026
655153f
Merge branch 'main' into codex/feat-mcp-2026-core
samuelhsin Aug 18, 2026
6cf2279
Merge branch 'main' into codex/feat-mcp-2026-core
samuelhsin Aug 18, 2026
0a96db3
Merge branch 'main' into codex/feat-mcp-2026-core
samuelhsin Aug 18, 2026
7e3d5cd
Merge branch 'main' into codex/feat-mcp-2026-core
samuelhsin Aug 18, 2026
33f4b0d
Merge branch 'main' into codex/feat-mcp-2026-core
samuelhsin Aug 18, 2026
2948703
Merge branch 'main' into codex/feat-mcp-2026-core
samuelhsin Aug 18, 2026
5488ad6
Merge branch 'main' into codex/feat-mcp-2026-core
samuelhsin Aug 19, 2026
a4f0b8f
Merge branch 'main' into codex/feat-mcp-2026-core
samuelhsin Aug 19, 2026
0816e57
fix(mcp): reuse session client for list and emit app fallback text
samuelhsin Aug 19, 2026
15b6651
Merge branch 'main' into codex/feat-mcp-2026-core
samuelhsin Aug 19, 2026
6dd6a5b
Merge branch 'main' into codex/feat-mcp-2026-core
samuelhsin Aug 19, 2026
0e0df61
Merge branch 'main' into codex/feat-mcp-2026-core
samuelhsin Aug 19, 2026
b5ddeec
Merge branch 'main' into codex/feat-mcp-2026-core
samuelhsin Aug 19, 2026
935b97c
Merge branch 'main' into codex/feat-mcp-2026-core
samuelhsin Aug 19, 2026
e284ec1
Merge branch 'main' into codex/feat-mcp-2026-core
samuelhsin Aug 19, 2026
9242dc1
Merge branch 'main' into codex/feat-mcp-2026-core
samuelhsin Aug 19, 2026
01e7821
Merge branch 'main' into codex/feat-mcp-2026-core
samuelhsin Aug 19, 2026
4e3a80f
Merge branch 'main' into codex/feat-mcp-2026-core
samuelhsin Aug 19, 2026
63186ec
Merge branch 'main' into codex/feat-mcp-2026-core
samuelhsin Aug 19, 2026
7e3118b
fix(mcp): keep mcp list and IPv6 sandbox CSP valid
Aug 19, 2026
f5e72bd
Merge branch 'main' into codex/feat-mcp-2026-core
samuelhsin Aug 20, 2026
4608aab
fix(mcp): keep modern list and short discovery budgets working
Aug 20, 2026
d00d97a
Merge branch 'main' into codex/feat-mcp-2026-core
samuelhsin Aug 20, 2026
838c5fc
Merge branch 'main' into codex/feat-mcp-2026-core
samuelhsin Aug 20, 2026
a2ad1e6
Merge branch 'main' into codex/feat-mcp-2026-core
samuelhsin Aug 20, 2026
946d1ea
Merge branch 'main' into codex/feat-mcp-2026-core
samuelhsin Aug 20, 2026
3c36402
Merge branch 'main' into codex/feat-mcp-2026-core
samuelhsin Aug 20, 2026
bef3782
Merge branch 'main' into codex/feat-mcp-2026-core
samuelhsin Aug 20, 2026
3adaf3c
Merge branch 'main' into codex/feat-mcp-2026-core
samuelhsin Aug 20, 2026
04ea167
fix(mcp): keep the 2026 slice free of review-only extras
Aug 20, 2026
895a604
Merge branch 'main' into codex/feat-mcp-2026-core
samuelhsin Aug 20, 2026
c6c7f56
Merge branch 'main' into codex/feat-mcp-2026-core
samuelhsin Aug 20, 2026
ef9b4d3
fix(mcp): restore generated companion notices after the SDK v2 bump
Aug 20, 2026
4a8f1c2
fix(mcp): isolate the Apps proxy from WebShell storage
Aug 20, 2026
5368117
Merge branch 'main' into codex/feat-mcp-2026-core
samuelhsin Aug 20, 2026
dce4d0b
Merge branch 'main' into codex/feat-mcp-2026-core
samuelhsin Aug 20, 2026
bb745d1
fix(mcp): harden fallback and app sandbox
samuelhsin Aug 20, 2026
3dc82b3
Merge remote-tracking branch 'upstream/main' into codex/feat-mcp-2026…
samuelhsin Aug 21, 2026
f9585c9
Merge remote-tracking branch 'upstream/main' into codex/feat-mcp-2026…
samuelhsin Aug 21, 2026
1ea10ee
Merge remote-tracking branch 'upstream/main' into codex/feat-mcp-2026…
samuelhsin Aug 21, 2026
ae76877
fix(core): preserve large and app-only MCP catalogs
samuelhsin Aug 21, 2026
91cb725
Merge branch 'main' into codex/feat-mcp-2026-core
samuelhsin Aug 22, 2026
0c6ffaa
Merge branch 'main' into codex/feat-mcp-2026-core
samuelhsin Aug 22, 2026
61d9e6d
fix(mcp): preserve legacy negotiation compatibility
Aug 22, 2026
54c2092
fix(mcp): default stdio negotiation to legacy
Aug 22, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
143 changes: 143 additions & 0 deletions docs/design/mcp-2026-core-client-foundation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
# MCP 2026 core client foundation

## Context

Qwen Code's configured MCP sessions currently use the v1 TypeScript SDK. A
server that only implements the MCP `2026-07-28` stateless protocol cannot
complete the legacy `initialize` handshake, while unconditionally switching to
the modern protocol would break existing servers.

The official TypeScript SDK v2 already owns the wire-level compatibility
logic: `server/discover` negotiation, legacy fallback, per-request metadata and
HTTP headers, pagination, and cache-hint handling. Qwen Code should configure
that behavior rather than duplicate it.

## Scope

This slice of #8968 migrates configured MCP sessions to the v2 client, adds
opt-in automatic protocol negotiation for stdio sessions, and adds the first
MCP Apps host for daemon-backed WebShell sessions. Tool, prompt, resource-list,
and resource-read operations use the v2 cache-aware helpers when the negotiated
protocol is modern.

Remote HTTP / SSE / TCP clients stay on `versionNegotiation.mode = 'legacy'`.
SDK v2 rejects HTTP `server/discover` probe timeouts with no `initialize`
fallback, so auto-negotiation would drop working remote servers that ignore
unknown pre-initialize methods. Connecting to a 2026-07-28-only remote server
is deferred until that SDK gap closes.

The following remain separate follow-ups:

- modern-only remote (HTTP / SSE / TCP) protocol negotiation;
- interactive MRTR elicitation and approval across TUI, WebShell, headless, and
ACP;
- MCP App initiated tool calls, links, downloads, messages, model-context
updates, and fullscreen display;
- migration of Qwen Code's internal IDE, Computer Use, and embedded MCP server
integrations, which are not configured external MCP sessions.

## Design

Configured stdio MCP clients default to `versionNegotiation.mode = 'legacy'`.
Setting `versionNegotiation: "auto"` opts a server into a `server/discover`
probe capped at 5s, and further shortened so the probe plus initialize fallback
still fit inside `discoveryTimeoutMs` (the discovery window clamp is
`[100ms, 300s]`; a budget that cannot cover both steps skips the probe and uses
`legacy`). Definitive modern evidence selects the stateless `2026-07-28`
protocol; legacy evidence — including a silent stdio server that never answers
the probe — falls back to the unchanged `initialize` flow.

The SDK performs opt-in stdio auto-negotiation on a disposable sibling process
before starting the session process, so the configured command runs twice per
connection. The default legacy policy skips the probe and retains the
single-process initialize flow for servers with non-idempotent startup side
effects or single-owner resources such as lockfiles.

Remote HTTP / SSE / TCP clients use `versionNegotiation.mode = 'legacy'` and
never send `server/discover`.

Modern sessions use the typed v2 list/read methods so the SDK can aggregate
pagination and honor `ttlMs` and `cacheScope`. Legacy sessions keep Qwen Code's
raw request path for prompts and resources because it intentionally tolerates
older servers that expose methods without declaring the matching capability.

Tool discovery uses the single cache-aware `tools/list` result for both schema
registration and annotations. Tool execution continues through the raw client
so progress, cancellation, timeout, permission checks, and output handling stay
inside the existing Qwen Code path.

Configured clients advertise the `io.modelcontextprotocol/ui` extension and
the `text/html;profile=mcp-app` resource type. When a server also advertises
that extension, tool discovery preserves its `ui://` resource URI. After a
successful call, Qwen Code reads and validates the matching HTML resource and
stores it in a structured display result while leaving the model-visible result
unchanged. A missing, oversized, malformed, or unreadable resource falls back
to the normal text result.

The daemon serves a static sandbox proxy before bearer authentication. It
contains no session data or credentials. WebShell loads that proxy in an
outer iframe that omits `allow-same-origin`, so even a same-URL `localhost`
load is an opaque origin and cannot read WebShell `sessionStorage`. When the
daemon is already on `127.0.0.1` or `[::1]`, the host also swaps onto
`localhost` for a second loopback origin. AppBridge and postMessage deliver
the validated HTML, tool input, and tool result to an inner sandboxed iframe.
The proxy validates parent and child origins, applies resource CSP as an HTTP
response header, and forwards AppBridge postMessage traffic between the two
frames. The host AppBridge schema-validates inbound messages; the proxy itself
does not filter payload shape. The inner App iframe also omits
`allow-same-origin`, giving untrusted HTML an opaque origin that cannot call
the daemon's loopback API as a same-origin client. The first host slice does
not advertise privileged App capabilities.

## Compatibility and safety

- No configured server is pinned to the modern protocol.
- Configured stdio servers use the single-process legacy flow by default and
can opt into the extra negotiation process with `versionNegotiation: "auto"`.
- Legacy fallback remains the SDK's byte-compatible v1 sequence.
- Authorization and Qwen Code's MCP permission boundary are unchanged.
- The modern cache is private per client instance; no result is shared across
workspaces or authorization principals.
- MCP App HTML is limited to 1 MiB and never enters model context.
- App HTML runs in a double-iframe sandbox. Both frames omit
`allow-same-origin`, and the outer frame additionally uses a different
loopback origin when one is available. Server-declared CSP is enforced by
the daemon response.
- If the isolation origin is unavailable, WebShell displays the ordinary tool
text rather than rendering the App.
- Compacted session history keeps `type: 'mcp_app'` with empty `html` and the
original `fallbackText`; WebShell renders that text instead of mounting an
empty sandbox.
- The host sends `ui/resource-teardown` and waits for it to settle before
unloading the sandbox iframe.

## Verification

- A modern-only control transport must connect through `server/discover`, list
and call a tool without `initialize`, and carry the modern request metadata.
- A real Streamable HTTP transport uses the legacy `initialize` handshake and
must still send the protocol and method headers, plus the tool name header
on `tools/call`. Modern-only remote negotiation is out of scope.
- A legacy control transport must fall back to `initialize` and retain existing
discovery and call behavior.
- A cache-hinted modern list result must be reused without a second wire
request.
- A mock stdio MCP server must advertise the Apps extension, return a `ui://`
dashboard resource, and render that dashboard inside an actual daemon-backed
WebShell transcript. The PR description includes the external test fixture
used for this verification without shipping it in the product repository.
- Compacted replay of an App result must show fallback text and must not mount
a sandbox iframe.
- Invalid App resource MIME types and unavailable resources must retain the
ordinary text result.
- The sandbox route must reject CSP directive injection and remain a static,
no-store pre-auth resource.
- Existing MCP client, transport-pool, tool, OAuth, and resource tests must
continue to pass, followed by the repository build and typecheck.

## Demo

The external stdio demo used for verification advertises one
`show_revenue_dashboard` tool and its `ui://revenue-dashboard` resource. Its
reference implementation and daemon configuration are included in the PR
description.
5 changes: 3 additions & 2 deletions docs/developers/daemon/05-mcp-transport-pool.md
Original file line number Diff line number Diff line change
Expand Up @@ -315,11 +315,12 @@ ordering.
The pool key comes from `fingerprint(cfg)` in `mcp-pool-key.ts`. The hash covers
all transport-defining fields:

> `transport, command, args, cwd, env, url, httpUrl, tcp, headers, timeout, oauth`
> `transport, command, args, cwd, env, url, httpUrl, tcp, headers, timeout, versionNegotiation, oauth`

Per-session filtering and metadata fields (`includeTools`, `excludeTools`,
`trust`, `description`, `extensionName`, `discoveryTimeoutMs`) are excluded, so
sessions with different filters can share one entry.
sessions with different filters can share one entry. The automatic negotiation
opt-in is included because it changes how the underlying process connects.

For the OAuth cell, `canonicalOAuth(o)` hashes every `MCPOAuthConfig` field:
`clientId`, `clientSecret`, sorted `scopes`, sorted `audiences`,
Expand Down
1 change: 1 addition & 0 deletions docs/developers/tools/mcp-server.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ Each server configuration supports the following properties:
- **`env`** (object): Environment variables for the server process. Values can reference environment variables using `$VAR_NAME` or `${VAR_NAME}` syntax
- **`cwd`** (string): Working directory for Stdio transport
- **`timeout`** (number): Request timeout in milliseconds (default: 600,000ms = 10 minutes)
- **`versionNegotiation`** (`"auto" | "legacy"`, default: `"legacy"`): For Stdio servers, `"auto"` opts into the `server/discover` probe on a disposable sibling process.
- **`trust`** (boolean): When `true`, bypasses tool call confirmations for this server in a trusted workspace (default: `false`)
- **`includeTools`** (string[]): List of tool names to include from this MCP server. When specified, only the tools listed here will be available from this server (allowlist behavior). If not specified, all tools from the server are enabled by default.
- **`excludeTools`** (string[]): List of tool names to exclude from this MCP server. Tools listed here will not be available to the model, even if they are exposed by the server. **Note:** `excludeTools` takes precedence over `includeTools` - if a tool is in both lists, it will be excluded.
Expand Down
47 changes: 35 additions & 12 deletions docs/users/features/mcp.md
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,28 @@ The existing `timeout` field is **tool-call** timeout (used for each
`discoveryTimeoutMs` — a long-running tool invocation is not a startup
pathology.

### Automatic stdio negotiation

Stdio servers use the single-process legacy initialize flow by default. To
connect to a modern-only stdio server, opt into automatic protocol negotiation:

```jsonc
{
"mcpServers": {
"modern-server": {
"command": "node",
"args": ["./server.js"],
"versionNegotiation": "auto",
},
},
}
```

Automatic negotiation runs a short-lived copy of the configured server before
starting the session process and can use up to five seconds of the discovery
budget. Keep the default legacy policy for servers with non-idempotent startup
side effects, single-owner locks or PID files, or slow initialize handshakes.

### Rolling back progressive MCP

If you need the old synchronous behavior (cli waits for every MCP server
Expand Down Expand Up @@ -454,18 +476,19 @@ Required (one of the following):

Optional:

| Property | Type/Default | Description |
| ---------------------- | ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `args` | array | Command-line arguments for Stdio transport |
| `headers` | object | Custom HTTP headers when using `url` or `httpUrl` |
| `env` | object | Environment variables for the server process. Values can reference environment variables using `$VAR_NAME` or `${VAR_NAME}` syntax |
| `cwd` | string | Working directory for Stdio transport |
| `timeout` | number<br>(default: 600,000) | Request timeout in milliseconds (default: 600,000ms = 10 minutes) |
| `trust` | boolean<br>(default: false) | When `true`, bypasses tool call confirmations for this server in a trusted workspace (default: `false`) |
| `includeTools` | array | List of tool names to include from this MCP server. When specified, only the tools listed here will be available from this server (allowlist behavior). If not specified, all tools from the server are enabled by default. |
| `excludeTools` | array | List of tool names to exclude from this MCP server. Tools listed here will not be available to the model, even if they are exposed by the server.<br>Note: `excludeTools` takes precedence over `includeTools` - if a tool is in both lists, it will be excluded. |
| `targetAudience` | string | The OAuth Client ID allowlisted on the IAP-protected application you are trying to access. Used with `authProviderType: 'service_account_impersonation'`. |
| `targetServiceAccount` | string | The email address of the Google Cloud Service Account to impersonate. Used with `authProviderType: 'service_account_impersonation'`. |
| Property | Type/Default | Description |
| ---------------------- | --------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `args` | array | Command-line arguments for Stdio transport |
| `headers` | object | Custom HTTP headers when using `url` or `httpUrl` |
| `env` | object | Environment variables for the server process. Values can reference environment variables using `$VAR_NAME` or `${VAR_NAME}` syntax |
| `cwd` | string | Working directory for Stdio transport |
| `timeout` | number<br>(default: 600,000) | Request timeout in milliseconds (default: 600,000ms = 10 minutes) |
| `versionNegotiation` | `"auto" \| "legacy"`<br>(default: `"legacy"`) | For Stdio servers, `"auto"` opts into protocol negotiation on a disposable sibling process. The default `"legacy"` starts only the session process. |
| `trust` | boolean<br>(default: false) | When `true`, bypasses tool call confirmations for this server in a trusted workspace (default: `false`) |
| `includeTools` | array | List of tool names to include from this MCP server. When specified, only the tools listed here will be available from this server (allowlist behavior). If not specified, all tools from the server are enabled by default. |
| `excludeTools` | array | List of tool names to exclude from this MCP server. Tools listed here will not be available to the model, even if they are exposed by the server.<br>Note: `excludeTools` takes precedence over `includeTools` - if a tool is in both lists, it will be excluded. |
| `targetAudience` | string | The OAuth Client ID allowlisted on the IAP-protected application you are trying to access. Used with `authProviderType: 'service_account_impersonation'`. |
| `targetServiceAccount` | string | The email address of the Google Cloud Service Account to impersonate. Used with `authProviderType: 'service_account_impersonation'`. |

<a id="qwen-mcp-cli"></a>

Expand Down
Loading
Loading