Tutorial
How to route MCP requests with Mcp-Method and Mcp-Name headers
The 2026-07-28 MCP spec requires Mcp-Method and Mcp-Name headers so gateways route MCP traffic without reading the body. Here is how, tested on Node 25.
Mark
Head of Marketing, MCPOrbit
- Published
- Updated
- · Updated
- Read time
- · 9 min read

The 2026-07-28 MCP spec (SEP-2243) requires two HTTP headers on every Streamable HTTP POST: Mcp-Method carries the JSON-RPC method (for example tools/call), and Mcp-Name carries the target name (for example get_order_status) on any request that names a tool, resource, or prompt. With the operation in the headers, a load balancer, gateway, or rate limiter can route and throttle MCP traffic without parsing the request body. Below is how to emit the headers from a client, act on them in a gateway, and validate them on the server, with TypeScript that runs on Node 25.
What problem do Mcp-Method and Mcp-Name solve?
A stateless MCP server (the model the 2026-07-28 spec pushes you toward) sits behind ordinary infrastructure: a load balancer, a proxy, a WAF, a rate limiter. That infrastructure needs to make decisions per operation. It wants to send resources/read to a read replica, or throttle one expensive tool, or count calls per tenant. Before SEP-2243 the only way to know what an MCP POST was doing was deep packet inspection: parse the JSON-RPC body at the edge. That is slow, brittle, and often impossible when the body is streamed or large.
SEP-2243 moves the routing-relevant facts into headers. Mcp-Method rides on every request. Mcp-Name rides on requests that name a tool, resource, or prompt. A gateway reads two headers and routes. It never touches the body.
How does a gateway route on the headers without reading the body?
It only ever looks at the two headers. Here is a small gateway that routes reads to a replica pool and rate-limits one named tool. Notice that the routing function is handed the headers and nothing else, so there is no way for it to parse a body even by accident:
// gateway.ts
// A gateway, load balancer, or rate limiter that acts on MCP traffic using
// ONLY the HTTP headers. It never parses the JSON-RPC body. That is the whole
// point of SEP-2243: the method and the target name ride in Mcp-Method and
// Mcp-Name, so infrastructure routes and throttles without deep packet
// inspection.
type RouteDecision =
| { action: "forward"; pool: string }
| { action: "reject"; status: number; reason: string };
export function makeGateway(opts: { limits?: Record<string, number> } = {}) {
const limits = opts.limits ?? {};
const counts = new Map<string, number>(); // tool name -> calls this window
// `route` is deliberately given only the headers, never the body, to prove
// no body parsing happens.
return function route(headers: Record<string, string>): RouteDecision {
const method = headers["mcp-method"];
const name = headers["mcp-name"];
// 1. Route by method. Reads go to replicas, everything else to primary.
const pool =
method === "resources/read" || method === "resources/list"
? "read-replicas"
: "primary";
// 2. Rate-limit a specific tool by name, header-only. A gateway can shed
// load on expensive_report without knowing anything else in the payload.
if (method === "tools/call" && name && name in limits) {
const n = (counts.get(name) ?? 0) + 1;
counts.set(name, n);
if (n > limits[name]) {
return { action: "reject", status: 429, reason: `rate limit exceeded for ${name}` };
}
}
return { action: "forward", pool };
};
}That is the whole point of SEP-2243 in one file. The gateway rate-limits expensive_report by reading Mcp-Name. It sends resources/read to read-replicas by reading Mcp-Method. A real gateway would do this in nginx, Envoy, or an API gateway config, but the logic is identical: match on Mcp-Method and Mcp-Name, act, forward.
How does a client attach the routing headers?
Set Mcp-Method on every request. Set Mcp-Name whenever the request names a tool, resource, or prompt. For x-mcp-header, copy the chosen tool argument into a custom header. This small helper does all three:
// client.ts
// Attaches the SEP-2243 routing headers to every Streamable HTTP POST.
// Mcp-Method rides on every request. Mcp-Name rides on requests that name a
// tool, resource, or prompt. x-mcp-header lifts a chosen tool argument into a
// custom header so infrastructure can route on it too.
type JsonRpc = {
jsonrpc: "2.0";
id: number | string;
method: string;
params?: { name?: string; arguments?: Record<string, unknown> };
};
export function buildRequest(
rpc: JsonRpc,
opts: { headerParams?: Record<string, string> } = {},
) {
const headers: Record<string, string> = {
"content-type": "application/json",
"mcp-method": rpc.method, // required on every POST
};
const name = rpc.params?.name;
if (name) headers["mcp-name"] = name; // only when a target is named
// x-mcp-header: promote declared tool arguments into custom headers so a
// gateway can route on them (for example, a tenant or region).
for (const [arg, headerName] of Object.entries(opts.headerParams ?? {})) {
const value = rpc.params?.arguments?.[arg];
if (value !== undefined) headers[headerName] = String(value);
}
return { headers, body: JSON.stringify(rpc) };
}How does the server stop a header from lying about the body?
Header-only routing is only safe if the header is trustworthy. If a client could send Mcp-Method: resources/read while the body actually calls a tool, a gateway would route it wrong and a rate limit would be trivial to bypass. SEP-2243 closes that hole: the server MUST reject a request whose headers disagree with the body, returning HeaderMismatchError with code -32020. The server is the enforcement point that lets the gateway trust the header:
// server.ts
// A stateless MCP request handler that trusts the SEP-2243 routing headers
// only after it has proven they agree with the JSON-RPC body.
// On disagreement it returns HeaderMismatchError (-32020), the code the
// 2026-07-28 spec assigns to this case.
export const HEADER_MISMATCH = -32020; // HeaderMismatchError (SEP-2243)
export const METHOD_NOT_FOUND = -32601;
export const INVALID_PARAMS = -32602;
type JsonRpc = {
jsonrpc: "2.0";
id: number | string;
method: string;
params?: { name?: string; arguments?: Record<string, unknown> };
};
const TOOLS: Record<string, (args: Record<string, unknown>) => unknown> = {
get_order_status: (args) => ({ orderId: args.id, status: "shipped" }),
expensive_report: (args) => ({ report: `report for ${args.region}`, rows: 10_000 }),
};
export function handleMcpRequest(headers: Record<string, string>, body: JsonRpc) {
const headerMethod = headers["mcp-method"];
const headerName = headers["mcp-name"];
const { id, method, params } = body;
// The contract that makes header-only routing safe: the server rejects any
// request whose headers lie about the body. A gateway can then act on the
// header alone, because the server guarantees the header matches the body.
if (headerMethod !== method) {
return err(id, HEADER_MISMATCH, `Mcp-Method "${headerMethod}" != body method "${method}"`);
}
const bodyName = params?.name;
if (bodyName !== undefined && headerName !== bodyName) {
return err(id, HEADER_MISMATCH, `Mcp-Name "${headerName}" != body params.name "${bodyName}"`);
}
if (method === "tools/call") {
const fn = TOOLS[params?.name as string];
if (!fn) return err(id, INVALID_PARAMS, `unknown tool ${params?.name}`);
return { jsonrpc: "2.0", id, result: { structuredContent: fn(params?.arguments ?? {}) } };
}
return err(id, METHOD_NOT_FOUND, `method not found: ${method}`);
}
function err(id: number | string, code: number, message: string) {
return { jsonrpc: "2.0", id, error: { code, message } };
}The validation is two comparisons: Mcp-Method against the body method, and Mcp-Name against params.name when a name is present. Any disagreement returns -32020 before the tool ever runs. Once this check is in place, a gateway upstream can act on the headers alone, because the server guarantees they match the body.
Does this actually work? (tested)
Yes. Routing headers are an HTTP transport concern, so the test uses plain Node with no MCP SDK dependency, which is honest: a load balancer or gateway does not run the MCP SDK either. The suite proves the client attaches the headers, the gateway routes and rate-limits on headers alone, x-mcp-header promotes an argument, and the server returns -32020 on any mismatch. Run against Node v25.8.1:
// routing.test.ts
import { test } from "node:test";
import assert from "node:assert/strict";
import { handleMcpRequest, HEADER_MISMATCH } from "./server.ts";
import { makeGateway } from "./gateway.ts";
import { buildRequest } from "./client.ts";
test("client attaches Mcp-Method on every request and Mcp-Name when a tool is named", () => {
const { headers } = buildRequest({
jsonrpc: "2.0",
id: 1,
method: "tools/call",
params: { name: "get_order_status", arguments: { id: "A-1" } },
});
assert.equal(headers["mcp-method"], "tools/call");
assert.equal(headers["mcp-name"], "get_order_status");
});
test("a request with no named target carries Mcp-Method but no Mcp-Name", () => {
const { headers } = buildRequest({ jsonrpc: "2.0", id: 2, method: "tools/list" });
assert.equal(headers["mcp-method"], "tools/list");
assert.equal(headers["mcp-name"], undefined);
});
test("gateway routes by method using headers only, never the body", () => {
const route = makeGateway();
// Note: route() is only ever handed headers. There is no body in scope.
assert.deepEqual(route({ "mcp-method": "resources/read", "mcp-name": "file://x" }), {
action: "forward",
pool: "read-replicas",
});
assert.deepEqual(route({ "mcp-method": "tools/call", "mcp-name": "get_order_status" }), {
action: "forward",
pool: "primary",
});
});
test("gateway rate-limits a tool by header name alone", () => {
const route = makeGateway({ limits: { expensive_report: 2 } });
const h = { "mcp-method": "tools/call", "mcp-name": "expensive_report" };
assert.equal(route(h).action, "forward"); // 1
assert.equal(route(h).action, "forward"); // 2
const third = route(h); // 3 -> over the limit
assert.equal(third.action, "reject");
assert.equal((third as { status: number }).status, 429);
});
test("x-mcp-header promotes a tool argument into a custom routing header", () => {
const { headers } = buildRequest(
{
jsonrpc: "2.0",
id: 3,
method: "tools/call",
params: { name: "expensive_report", arguments: { region: "eu-west-1" } },
},
{ headerParams: { region: "x-mcp-header-region" } },
);
assert.equal(headers["x-mcp-header-region"], "eu-west-1");
});
test("server rejects a Mcp-Method that disagrees with the body (-32020)", () => {
const res = handleMcpRequest(
{ "mcp-method": "resources/read", "mcp-name": "get_order_status" }, // header lies
{ jsonrpc: "2.0", id: 4, method: "tools/call", params: { name: "get_order_status" } },
);
assert.equal((res as any).error.code, HEADER_MISMATCH);
});
test("server rejects a Mcp-Name that disagrees with the body (-32020)", () => {
const res = handleMcpRequest(
{ "mcp-method": "tools/call", "mcp-name": "delete_everything" }, // name lies
{ jsonrpc: "2.0", id: 5, method: "tools/call", params: { name: "get_order_status" } },
);
assert.equal((res as any).error.code, HEADER_MISMATCH);
});
test("server dispatches when headers and body agree (client -> gateway -> server)", () => {
const rpc = {
jsonrpc: "2.0" as const,
id: 6,
method: "tools/call",
params: { name: "get_order_status", arguments: { id: "A-42" } },
};
const { headers, body } = buildRequest(rpc);
const decision = makeGateway()(headers);
assert.equal(decision.action, "forward");
const res = handleMcpRequest(headers, JSON.parse(body));
assert.deepEqual((res as any).result.structuredContent, { orderId: "A-42", status: "shipped" });
});tests 8 · pass 8 · fail 0 on Node v25.8.1 (TypeScript run directly, no external dependencies)
One-command run
- Save server.ts, gateway.ts, client.ts, and routing.test.ts side by side.
- Run node --test on Node 25, which executes the TypeScript files directly.
- Expect tests 8 · pass 8 · fail 0.
FAQ
Frequently asked questions
- What are the Mcp-Method and Mcp-Name headers in MCP?
- They are HTTP headers added by SEP-2243 in the 2026-07-28 MCP spec, required on Streamable HTTP POST requests. Mcp-Method carries the JSON-RPC method (for example tools/call). Mcp-Name carries the target name (for example get_order_status) on requests that name a tool, resource, or prompt. They let gateways route and rate-limit on the operation without parsing the body.
- Why not just parse the JSON-RPC body at the gateway?
- Parsing the body at the edge is deep packet inspection: slow, brittle, and sometimes impossible when the body is large or streamed. Putting the method and target name in headers lets a load balancer, proxy, or rate limiter make decisions from two header reads, so MCP traffic routes like any other HTTP traffic.
- What happens if the headers disagree with the body?
- The server must reject the request with HeaderMismatchError, JSON-RPC error code -32020 in the 2026-07-28 spec. That guarantee is what makes header-only routing safe: a gateway can trust Mcp-Method and Mcp-Name because the server refuses any request where they do not match the body.
- What is the x-mcp-header mechanism?
- SEP-2243 also lets a server declare that a specific tool argument should be copied into a custom request header, using the x-mcp-header convention. This lets infrastructure route on values like tenant or region that live inside the tool arguments, without the gateway parsing the body.
- Does the MCP SDK attach these headers for me?
- Not on the current stable SDK line, which predates the 2026-07-28 spec. You attach Mcp-Method and Mcp-Name at your HTTP layer today, as shown in the tested client above. The header names and behavior are fixed by SEP-2243, so a later SDK with first-class support uses the same wire format.
- Are Mcp-Method and Mcp-Name required or optional?
- Required on Streamable HTTP POST requests in the 2026-07-28 spec. Mcp-Method is on every request. Mcp-Name is on every request that names a tool, resource, or prompt. A server that follows the spec validates both against the body and rejects mismatches with -32020.
About the author
Mark
Head of Marketing, MCPOrbit
Mark leads marketing at MCPOrbit and writes the build-it MCP tutorials, code tested end to end before it ships.


