Skip to content

fix(server): stop rejecting agent MCP requests over an unadvertised protocol version header - #4375

Open
smartenok-ops wants to merge 1 commit into
getpaseo:mainfrom
smartenok-ops:fix/agent-mcp-protocol-version-header
Open

fix(server): stop rejecting agent MCP requests over an unadvertised protocol version header#4375
smartenok-ops wants to merge 1 commit into
getpaseo:mainfrom
smartenok-ops:fix/agent-mcp-protocol-version-header

Conversation

@smartenok-ops

Copy link
Copy Markdown

Fixes #3599 (the protocol-rejection half).

Problem

Clients that send their preferred MCP protocol version in the MCP-Protocol-Version header of post-initialize requests — instead of the negotiated one — get every tools/list / tools/call rejected with 400 "Unsupported protocol version" and never see the injected Paseo tools.

  • Codex CLI >= 0.148 does this and does not retry, so a Paseo-managed Codex agent silently loses the whole injected tool surface (create_agent, notifyOnFinish, everything).
  • Claude Code >= 2.1.257 sends the same non-conforming header but recovers after one rejected request, which is why Claude agents look fine while Codex agents are broken.
  • The bundled @modelcontextprotocol/sdk 1.29.0 (and the current latest 1.30.0) advertises versions only up to 2025-11-25, while these clients speak 2026-07-28. No SDK release accepts that header today, so the daemon cannot negotiate or tolerate it without help.

Fix

The header is a post-handshake sanity echo, not a negotiation point: initialize already negotiates down to a version this server supports, and the stateless agent MCP transport serves every request independently. So in runAgentMcpRequest, when the header value is not one the SDK advertises, drop it (log a warn) and let the request proceed. Advertised values pass through untouched.

Two implementation details worth flagging:

  • The check is against the SDK's own SUPPORTED_PROTOCOL_VERSIONS import, so when the SDK is eventually bumped to know 2026-07-28, this path simply stops firing for it.
  • The header must be stripped from both req.headers and req.rawHeaders — the SDK's node-to-web-standard adapter (@hono/node-server getRequestListener) builds the fetch Request from rawHeaders, so deleting only the parsed headers has no effect.

QA evidence

Against a production daemon (Linux x86_64, daemon 0.7.1, daemon.mcp.injectIntoAgents: true, Codex CLI 0.153.3), probing /mcp/agents directly with raw JSON-RPC:

request before after
initialize with protocolVersion: 2026-07-28 200, negotiated 2025-11-25 200, negotiated 2025-11-25 (unchanged)
tools/list with MCP-Protocol-Version: 2026-07-28 400 Unsupported protocol version 200, full tool list
tools/list with MCP-Protocol-Version: 2025-11-25 200 200 (unchanged)
tools/list with no header 200 200 (unchanged)

After applying the same change on the live daemon, a Paseo-managed Codex orchestrator session regained the injected mcp__paseo__* tools and subagent finish notifications started arriving; the daemon log shows no further Unsupported protocol version errors.

Tests

  • New e2e regression test in agent-mcp.e2e.test.ts: initialize with a never-advertised version (2999-01-01) must still negotiate down to an advertised version, and a following tools/list carrying that version in MCP-Protocol-Version must return the tool list instead of the 400. Reverting the fix makes this test fail with exactly expected 400 to be 200.
  • agent-mcp.e2e.test.ts: the new test passes; the two pre-existing failures (create_agent auto-injects paseo MCP by default and can be disabled, create_agent injects a loopback MCP URL...) fail identically on unmodified main — not affected by this change.
  • bootstrap.smoke.test.ts: 21/21 pass.
  • tsc -p tsconfig.server.typecheck.json --noEmit: clean.

Tested on Linux x86_64 only (headless daemon); I did not test macOS or the desktop app.

…rotocol version header

Codex CLI >= 0.148 (and Claude Code >= 2.1.257, which recovers on retry)
sends its *preferred* MCP protocol version in the MCP-Protocol-Version
header of post-initialize requests instead of the negotiated one. The
bundled @modelcontextprotocol/sdk only advertises versions up to its own
LATEST_PROTOCOL_VERSION, so the transport answers every tools/list and
tools/call with 400 "Unsupported protocol version" and the client never
sees the injected Paseo tools. Codex does not retry, so its agents
silently lose the whole tool surface (getpaseo#3599).

The header is a post-handshake sanity echo, not a negotiation point:
initialize already negotiated a version this server supports, and the
stateless agent MCP transport serves every request independently. Drop
unadvertised header values (both from req.headers and req.rawHeaders,
which the SDK's node-to-web-standard adapter actually reads) so
misbehaving clients keep working; advertised values pass through
untouched and a warn is logged for the drop.
@greptile-apps

greptile-apps Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR makes the stateless agent MCP endpoint tolerate clients that echo a protocol version newer than the bundled SDK advertises.

  • Removes an unadvertised MCP-Protocol-Version value from both parsed and raw Node request headers before SDK transport handling.
  • Preserves supported protocol-version headers unchanged.
  • Adds an end-to-end regression test covering initialization negotiation followed by tools/list with a future-version header.
  • The behavior appears correct, but the compatibility tag and repository test-shape/type-boundary requirements still need attention.

Confidence Score: 4/5

The behavioral fix appears sound, but the explicit repository requirements for compatibility tagging, boundary validation, and test shape must be satisfied before merging.

No runtime correctness or security failure remains established; the accepted findings are repository-rule violations in the new compatibility shim and regression test.

Files Needing Attention: packages/server/src/server/bootstrap.ts; packages/server/src/server/agent/agent-mcp.e2e.test.ts

Important Files Changed

Filename Overview
packages/server/src/server/bootstrap.ts Correctly normalizes unsupported protocol-version headers before transport handling, but the compatibility shim lacks the repository-required cleanup tag.
packages/server/src/server/agent/agent-mcp.e2e.test.ts Covers the client-visible regression through the real endpoint, but uses unchecked response assertions and embeds protocol framing in the test body.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  A[MCP client initializes with preferred version] --> B[SDK negotiates a supported version]
  B --> C[Client sends post-initialize request]
  C --> D{Header advertised by SDK?}
  D -- Yes --> E[Preserve header]
  D -- No --> F[Warn and remove parsed and raw header]
  E --> G[SDK transport handles request]
  F --> G
  G --> H[Paseo tools returned]
Loading

Reviews (1): Last reviewed commit: "fix(server): stop rejecting agent MCP re..." | Re-trigger Greptile

Comment on lines +1511 to +1524
// Some MCP clients (Codex CLI >= 0.148, Claude Code >= 2.1.257) send
// their *preferred* protocol version in the MCP-Protocol-Version
// header on post-initialize requests instead of the *negotiated* one
// (#3599). The bundled SDK only advertises versions up to its own
// LATEST_PROTOCOL_VERSION, so every tools/list and tools/call is then
// rejected with 400 "Unsupported protocol version". Claude Code
// recovers by retrying with an accepted value; Codex does not, and
// its agents silently lose the injected Paseo tools.
//
// The header is a post-handshake sanity echo, not a negotiation
// point: initialize already negotiated a version this server
// supports, and this stateless transport serves every request
// independently. Drop unadvertised header values so misbehaving
// clients keep working; advertised values pass through untouched.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Compatibility shim lacks tag

This client-specific compatibility shim is documented with issue prose but has no // COMPAT(name): added in vX, remove after <date> tag. The repository requires every compatibility shim to use this tag so it appears in the cleanup backlog; this requirement must be satisfied before merging.

Context Used: CLAUDE.md (source)

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Comment on lines +102 to +109
const payload = JSON.parse(dataLine ? dataLine.slice("data:".length).trim() : body);
if (payload?.error) {
throw new Error(`MCP response carried an error: ${JSON.stringify(payload.error)}`);
}
if (payload?.result === undefined || typeof payload.result !== "object") {
throw new Error(`MCP response had no result object: ${body.slice(0, 200)}`);
}
return payload.result as Record<string, unknown>;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Response shape is unchecked

parseJsonRpcResult parses a network response with JSON.parse and then converts payload.result to Record<string, unknown> through a type assertion. The test later makes another unchecked assertion before reading the tools array. This violates the repository requirement to validate data at network boundaries and must be addressed before merging.

Rule Used: # Code Review Pattern Reference: Slop, Tests, Feat... (source)

Comment on lines +289 to +305
const initialize = await fetch(mcpUrl, {
method: "POST",
headers: {
"content-type": "application/json",
accept: "application/json, text/event-stream",
},
body: JSON.stringify({
jsonrpc: "2.0",
id: 1,
method: "initialize",
params: {
protocolVersion: futureVersion,
capabilities: {},
clientInfo: { name: "future-client", version: "1.0.0" },
},
}),
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Test embeds protocol framing

The regression test constructs raw HTTP and JSON-RPC payloads directly in the test body for both initialization here and tools/list at lines 314–326. Repository test guidance requires protocol mechanics to live in helpers so the test reads as setup, domain-level actions, and assertions; this requirement must be satisfied before merging.

Rule Used: # Code Review Pattern Reference: Slop, Tests, Feat... (source)

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug: Codex 0.148.0 MCP handshake uses unsupported protocol 2026-07-28

1 participant