What happened?
There is no way to make the v2 client emit Mcp-Param-* headers on a legacy-era connection — mirroring is gated on the modern era (packages/client/src/client/client.ts, mirroringActive = this.getProtocolEra() === 'modern' && …), and a warm tools/list cache or an explicit options.toolDefinition doesn't change that. That gate matches the spec (x-mcp-header is 2026-07-28-only), but it collides with a large real-world server:
GitHub's hosted MCP server (https://api.githubcopilot.com/mcp/) answers server/discover with -32601, so versionNegotiation: { mode: 'auto' } lands on the legacy initialize handshake at 2025-11-25 — and it then requires Mcp-Param-* on tools/call over that same legacy session, rejecting with -32020 (missing Mcp-Param-owner header) for any tool whose schema carries x-mcp-header (most of its catalog annotates owner/repo). Observed with a PAT in early August 2026 from a non-SDK client; the era gate means the v2 Client sends no headers on that path either, and 1.30.0 has no Mcp-Param support at all — so as far as I can tell no published SDK version can call those tools on that server today.
SEP-2243's backward-compatibility section permits this server behavior ("Servers MAY support older clients by accepting requests without headers when negotiating an older protocol version" — GitHub declines the MAY). So both sides are within spec, and the interop hole is real.
Two questions rather than a demand:
- Is per-request
options.headers the intended escape hatch? It works — RESERVED_REQUEST_HEADER_NAMES doesn't cover mcp-param-*, so a caller can run scanXMcpHeaderDeclarations/buildMcpParamHeaders themselves and pass the result per call. But those functions live in core-internal (private: true), so today that means vendoring mcpParamHeaders.ts. If this is the blessed path, exporting the codec (or documenting the vendoring) would close this issue.
- Would you take an opt-in for legacy-era mirroring — e.g. a
mirrorMcpParamHeaders: 'auto' | 'always' | 'never' client option, or mirroring whenever options.toolDefinition is explicitly supplied? Opt-in seems right rather than a default: SEP-2243's intermediary note says infrastructure on older negotiated versions SHOULD reject requests carrying header values it can't validate, so unconditional emission could break other servers. Happy to PR whichever shape you'd accept.
What did you expect?
Some supported way — even opt-in — to satisfy a server that enforces SEP-2243 header/body validation on a legacy-era connection, or an exported/documented path to build the headers myself.
Code to reproduce
Self-contained (no GitHub credentials needed): one server, two runs. Modern negotiation emits the headers; the legacy default emits none, with the same warm cache and the same explicit toolDefinition.
// node repro.mjs — @modelcontextprotocol/{client,server,node} 2.0.0
import http from "node:http";
import { Client, StreamableHTTPClientTransport } from "@modelcontextprotocol/client";
import { McpServer, fromJsonSchema, createMcpHandler } from "@modelcontextprotocol/server";
import { toNodeHandler } from "@modelcontextprotocol/node";
const inputSchema = {
type: "object",
properties: {
owner: { type: "string", "x-mcp-header": "owner" },
repo: { type: "string", "x-mcp-header": "repo" },
},
required: ["owner", "repo"],
};
function buildServer() {
const server = new McpServer({ name: "upstream", version: "0.0.0" });
server.registerTool(
"get_file_contents",
{ description: "Reads a file.", inputSchema: fromJsonSchema(inputSchema) },
async () => ({ content: [{ type: "text", text: "ok" }] }),
);
return server;
}
const nodeHandler = toNodeHandler(createMcpHandler(() => buildServer(), { legacy: "stateless" }));
const seen = [];
const httpServer = http.createServer((req, res) => {
seen.push(req.headers);
void nodeHandler(req, res);
});
await new Promise((r) => httpServer.listen(0, "127.0.0.1", r));
const url = new URL(`http://127.0.0.1:${httpServer.address().port}/`);
async function run(negotiation) {
seen.length = 0;
const client = new Client(
{ name: "repro", version: "0.0.0" },
negotiation ? { versionNegotiation: negotiation } : {},
);
await client.connect(new StreamableHTTPClientTransport(url));
const tool = (await client.listTools()).tools.find((t) => t.name === "get_file_contents");
await client.callTool(
{ name: "get_file_contents", arguments: { owner: "octo", repo: "hello" } },
undefined,
{ toolDefinition: tool }, // explicit definition; same result without it
);
await client.close();
const call = seen.find((h) => h["mcp-method"] === "tools/call") ?? seen.at(-1);
return {
negotiated: negotiation ? "modern (mode:auto)" : "legacy (default)",
protocolVersionHeader: call["mcp-protocol-version"],
mcpParamHeaders: Object.fromEntries(Object.entries(call).filter(([k]) => k.startsWith("mcp-param-"))),
};
}
console.log(await run({ mode: "auto" }));
console.log(await run(undefined));
httpServer.close();
Output:
{ negotiated: 'modern (mode:auto)', protocolVersionHeader: '2026-07-28',
mcpParamHeaders: { 'mcp-param-owner': 'octo', 'mcp-param-repo': 'hello' } }
{ negotiated: 'legacy (default)', protocolVersionHeader: '2025-11-25',
mcpParamHeaders: {} }
SDK version
@modelcontextprotocol/client@2.0.0, @modelcontextprotocol/server@2.0.0, @modelcontextprotocol/node@2.0.0 (also checked @modelcontextprotocol/sdk@1.30.0: no Mcp-Param support). Node 26.7.0.
Area
Client
What happened?
There is no way to make the v2 client emit
Mcp-Param-*headers on a legacy-era connection — mirroring is gated on the modern era (packages/client/src/client/client.ts,mirroringActive = this.getProtocolEra() === 'modern' && …), and a warmtools/listcache or an explicitoptions.toolDefinitiondoesn't change that. That gate matches the spec (x-mcp-headeris2026-07-28-only), but it collides with a large real-world server:GitHub's hosted MCP server (
https://api.githubcopilot.com/mcp/) answersserver/discoverwith-32601, soversionNegotiation: { mode: 'auto' }lands on the legacyinitializehandshake at2025-11-25— and it then requiresMcp-Param-*ontools/callover that same legacy session, rejecting with-32020(missing Mcp-Param-owner header) for any tool whose schema carriesx-mcp-header(most of its catalog annotatesowner/repo). Observed with a PAT in early August 2026 from a non-SDK client; the era gate means the v2Clientsends no headers on that path either, and 1.30.0 has noMcp-Paramsupport at all — so as far as I can tell no published SDK version can call those tools on that server today.SEP-2243's backward-compatibility section permits this server behavior ("Servers MAY support older clients by accepting requests without headers when negotiating an older protocol version" — GitHub declines the MAY). So both sides are within spec, and the interop hole is real.
Two questions rather than a demand:
options.headersthe intended escape hatch? It works —RESERVED_REQUEST_HEADER_NAMESdoesn't covermcp-param-*, so a caller can runscanXMcpHeaderDeclarations/buildMcpParamHeadersthemselves and pass the result per call. But those functions live incore-internal(private: true), so today that means vendoringmcpParamHeaders.ts. If this is the blessed path, exporting the codec (or documenting the vendoring) would close this issue.mirrorMcpParamHeaders: 'auto' | 'always' | 'never'client option, or mirroring wheneveroptions.toolDefinitionis explicitly supplied? Opt-in seems right rather than a default: SEP-2243's intermediary note says infrastructure on older negotiated versions SHOULD reject requests carrying header values it can't validate, so unconditional emission could break other servers. Happy to PR whichever shape you'd accept.What did you expect?
Some supported way — even opt-in — to satisfy a server that enforces SEP-2243 header/body validation on a legacy-era connection, or an exported/documented path to build the headers myself.
Code to reproduce
Self-contained (no GitHub credentials needed): one server, two runs. Modern negotiation emits the headers; the legacy default emits none, with the same warm cache and the same explicit
toolDefinition.Output:
SDK version
@modelcontextprotocol/client@2.0.0,@modelcontextprotocol/server@2.0.0,@modelcontextprotocol/node@2.0.0(also checked@modelcontextprotocol/sdk@1.30.0: noMcp-Paramsupport). Node 26.7.0.Area
Client