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:
parent
03842cd9ca
commit
75dc7ea6e2
5 changed files with 328 additions and 43 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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 } })
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
Loading…
Reference in a new issue