modelcontextprotocol / modelcontextprotocol/typescript-sdk
[v2] No supported way to emit Mcp-Param-* headers on a legacy-era connection, but GitHub's hosted server requires them there
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 13.4k
- Forks
- 2.2k
- Avg merge
- 3d 15h
- Merged PRs (30d)
- 4
Description
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.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. - Would you take an opt-in for legacy-era mirroring — e.g. a
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.
// 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
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with packages/client/src/client/client.ts, especially the mirroringActive protocol-era gate, then inspect the core-internal mcpParamHeaders.ts helpers and the supplied Node reproduction. Compare the modern and legacy negotiation paths and verify behavior with the reproduction. Done means the client has an agreed supported or documented path for MCP parameter headers on legacy connections.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- node.js, typescript
- Domain
- api, networking
- Issue type
- Feature
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 52/100