fix(server): reject a modern POST without the required MCP-Protocol-Version header (#2590)

Co-authored-by: Konstantin Konstantinov <konstantin@mach5technology.com>
Co-authored-by: Konstantin Konstantinov <KKonstantinov@users.noreply.github.com>
This commit is contained in:
David Pavlovschii 2026-08-17 09:31:47 +03:00 • committed by GitHub
commit 75dc7ea6e2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 328 additions and 43 deletions

View file

@ -15,7 +15,12 @@ import { PassThrough } from 'node:stream';
import type { Client } from '@modelcontextprotocol/client';
import { SSEClientTransport, StreamableHTTPClientTransport } from '@modelcontextprotocol/client';
import { CLIENT_CAPABILITIES_META_KEY, CLIENT_INFO_META_KEY, PROTOCOL_VERSION_META_KEY } from '@modelcontextprotocol/core-internal';
import {
CLIENT_CAPABILITIES_META_KEY,
CLIENT_INFO_META_KEY,
encodeMcpParamValue,
PROTOCOL_VERSION_META_KEY
} from '@modelcontextprotocol/core-internal';
import type {
CreateMcpHandlerOptions,
EventStore,
@ -356,6 +361,25 @@ export function modernEnvelopeMeta(clientInfo?: Implementation): Record<string,
};
}
/**
* The SEP-2243 standard request headers a conformant 2026-07-28 client sends
* alongside {@linkcode modernEnvelopeMeta}, for scenario bodies that put raw
* HTTP requests on the wire. `MCP-Protocol-Version` and `Mcp-Method` are
* required on every modern *request* POST (notification POSTs are exempt);
* `Mcp-Name` is additionally required for the methods that mirror
* `params.name` / `params.uri`, and carries the `=?base64?…?=` sentinel
* encoding the shipped client applies to it, so a name that is not a safe
* plain-ASCII field value reaches the server exactly as that client would
* send it rather than throwing in `Headers` construction.
*/
export function modernStandardHeaders(method: string, name?: string): Record<string, string> {
return {
'mcp-protocol-version': MODERN_REVISION,
'mcp-method': method,
...(name !== undefined && { 'mcp-name': encodeMcpParamValue(name) })
};
}
/**
* Fail fast if an entryModern connection did not actually negotiate the
* 2026-07-28 revision. Every cell on the arm asserts modern-path behavior, so

View file

@ -31,7 +31,7 @@ import { createMcpHandler, isLegacyRequest, McpServer, WebStandardStreamableHTTP
import { expect, vi } from 'vitest';
import { z } from 'zod/v4';
import { modernEnvelopeMeta } from '../helpers/index';
import { modernEnvelopeMeta, modernStandardHeaders } from '../helpers/index';
import { verifies } from '../helpers/verifies';
const LEGACY = '2025-11-25';
@ -172,8 +172,7 @@ verifies('typescript:hosting:entry:byo-sessionful-legacy', async () => {
headers: {
'content-type': 'application/json',
accept: 'application/json, text/event-stream',
'mcp-method': 'tools/call',
'mcp-name': 'greet'
...modernStandardHeaders('tools/call', 'greet')
},
body: JSON.stringify({
jsonrpc: '2.0',

View file

@ -35,7 +35,7 @@ import {
import { expect, vi } from 'vitest';
import { z } from 'zod/v4';
import { modernEnvelopeMeta, tapWire, wire } from '../helpers/index';
import { modernEnvelopeMeta, modernStandardHeaders, tapWire, wire } from '../helpers/index';
import { verifies } from '../helpers/verifies';
import type { TestArgs } from '../types';
@ -1158,8 +1158,7 @@ verifies('protocol:meta:server-identity', async ({ transport }: TestArgs) => {
headers: {
'content-type': 'application/json',
accept: 'application/json, text/event-stream',
'mcp-protocol-version': '2026-07-28',
'mcp-method': 'server/discover'
...modernStandardHeaders('server/discover')
},
body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'server/discover', params: { _meta: modernEnvelopeMeta() } })
});
@ -1184,8 +1183,7 @@ verifies('protocol:envelope:client-info-optional', async ({ transport }: TestArg
headers: {
'content-type': 'application/json',
accept: 'application/json, text/event-stream',
'mcp-protocol-version': '2026-07-28',
'mcp-method': 'tools/list'
...modernStandardHeaders('tools/list')
},
body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'tools/list', params: { _meta: meta } })
});

View file

@ -11,7 +11,7 @@ import { createMcpHandler, McpServer, SUBSCRIPTION_ID_META_KEY } from '@modelcon
import { expect } from 'vitest';
import { z } from 'zod/v4';
import { modernEnvelopeMeta, wire } from '../helpers/index';
import { modernEnvelopeMeta, modernStandardHeaders, wire } from '../helpers/index';
import { verifies } from '../helpers/verifies';
import type { TestArgs } from '../types';
@ -72,7 +72,7 @@ verifies('subscriptions:listen:ack-first-stamped', async () => {
headers: {
'Content-Type': 'application/json',
Accept: 'application/json, text/event-stream',
'mcp-method': 'subscriptions/listen'
...modernStandardHeaders('subscriptions/listen')
},
body: JSON.stringify({
jsonrpc: '2.0',
@ -270,7 +270,7 @@ verifies('subscriptions:listen:capacity-guard', async () => {
headers: {
'Content-Type': 'application/json',
Accept: 'application/json, text/event-stream',
'mcp-method': 'subscriptions/listen'
...modernStandardHeaders('subscriptions/listen')
},
body: JSON.stringify({
jsonrpc: '2.0',

View file

@ -173,6 +173,7 @@ describe('createMcpHandler over HTTP — subscriptions/listen honored filter', (
headers: {
'Content-Type': 'application/json',
Accept: 'application/json, text/event-stream',
'mcp-protocol-version': MODERN,
'mcp-method': 'subscriptions/listen'
},
body: JSON.stringify({