Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 23 additions & 3 deletions examples/servers/typescript/everything-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1225,6 +1225,16 @@ app.use(
})
);

// Protocol revisions that use the initialize/session lifecycle. The
// per-request `_meta` and header/body validation requirements apply to
// 2026-07-28 and later, not to traffic from these revisions.
const LEGACY_SESSION_PROTOCOL_VERSIONS = [
'2024-11-05',
'2025-03-26',
'2025-06-18',
'2025-11-25'
];

// Handle POST requests - stateful mode
app.post('/mcp', async (req, res) => {
const sessionId = req.headers['mcp-session-id'] as string | undefined;
Expand All @@ -1236,7 +1246,15 @@ app.post('/mcp', async (req, res) => {
const meta = params._meta;
const metaVersion = meta?.['io.modelcontextprotocol/protocolVersion'];

if (!sessionId && (reqVersion || meta)) {
// A request that carries no `_meta` and names a legacy session-era revision
// in the header is legacy traffic; it is served by the session path below
// instead of being rejected for missing per-request metadata.
const isLegacySessionEraRequest =
meta === undefined &&
reqVersion !== undefined &&
LEGACY_SESSION_PROTOCOL_VERSIONS.includes(reqVersion);

if (!sessionId && (reqVersion || meta) && !isLegacySessionEraRequest) {
// Missing Transport Header Validation Check
if (!reqVersion) {
return res.status(400).json({
Expand All @@ -1246,14 +1264,16 @@ app.post('/mcp', async (req, res) => {
});
}

// Per-Request Metadata Integrity Checks (Fields verification)
// Per-Request Metadata Integrity Checks (Fields verification).
// A request missing any required `_meta` field is malformed: -32602 and,
// on HTTP, status 400 Bad Request.
if (
!meta ||
!meta['io.modelcontextprotocol/protocolVersion'] ||
!meta['io.modelcontextprotocol/clientInfo'] ||
!meta['io.modelcontextprotocol/clientCapabilities']
) {
return res.status(200).json({
return res.status(400).json({
jsonrpc: '2.0',
id,
error: {
Expand Down
87 changes: 87 additions & 0 deletions src/scenarios/server/stateless.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,93 @@ describe('Stateless Server Scenario Negative Tests', () => {
expect(missingVersionCheck?.status).toBe('FAILURE');
});

test('Fails validation when missing-_meta rejections are returned with HTTP 200', async () => {
// This bad server picks the right JSON-RPC error code but the wrong HTTP
// status: the spec requires 400 Bad Request for malformed requests.
const mockUrl = mockFetchTarget((reqBody) => {
const meta = reqBody.params?._meta;
const missingRequired =
!meta ||
!meta['io.modelcontextprotocol/protocolVersion'] ||
!meta['io.modelcontextprotocol/clientInfo'] ||
!meta['io.modelcontextprotocol/clientCapabilities'];
if (missingRequired) {
return {
status: 200, // Spec Violation: must be HTTP 400
body: {
jsonrpc: '2.0',
id: reqBody.id,
error: {
code: -32602,
message: 'Invalid params: missing _meta or required fields'
}
}
};
}
});

const scenario = new ServerStatelessScenario();
const checks = await scenario.run(testContext(mockUrl));

// The JSON-RPC code is correct, so the per-field checks pass; the wrong
// HTTP status is caught by the companion status check.
const missingMetaCheck = findCheck(
checks,
'sep-2575-request-meta-invalid-missing-meta'
);
const httpStatusCheck = findCheck(
checks,
'sep-2575-http-server-meta-invalid-400'
);

expect(missingMetaCheck?.status).toBe('SUCCESS');
expect(httpStatusCheck?.status).toBe('FAILURE');
});

test('Fails validation when the unsupported-version error omits data.requested', async () => {
const mockUrl = mockFetchTarget((reqBody) => {
const meta = reqBody.params?._meta;
if (meta?.['io.modelcontextprotocol/protocolVersion'] === 'v999.0.0') {
return {
status: 400,
body: {
jsonrpc: '2.0',
id: reqBody.id,
error: {
code: -32004,
message: 'Unsupported protocol version',
// Spec Violation: data.requested is a required member
data: { supported: ['2026-07-28'] }
}
}
};
}
if (reqBody.method === 'server/discover') {
return {
status: 200,
body: {
jsonrpc: '2.0',
id: reqBody.id,
result: {
supportedVersions: ['2026-07-28'],
capabilities: {},
serverInfo: { name: 'no-requested-server', version: '1.0.0' }
}
}
};
}
});

const scenario = new ServerStatelessScenario();
const checks = await scenario.run(testContext(mockUrl));

const negotiationCheck = findCheck(
checks,
'sep-2575-server-unsupported-version-error'
);
expect(negotiationCheck?.status).toBe('FAILURE');
});

test('Fails validation if removed legacy RPCs do not return HTTP 404 Not Found', async () => {
// This bad server intercepts the removed 'ping' or 'initialize' methods but incorrectly returns HTTP 200
const mockUrl = mockFetchTarget((reqBody) => {
Expand Down
70 changes: 53 additions & 17 deletions src/scenarios/server/stateless.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ export class ServerStatelessScenario implements ClientScenario {

**Grouped Specification Requirements**:

1. **Per-Request _meta Validation (4 Checks)**
- Rejects requests missing \`_meta\` or lacking structural required internal subfields (\`protocolVersion\`, \`clientInfo\`, \`clientCapabilities\`) with a JSON-RPC \`-32602 Invalid params\` error signature.
1. **Per-Request _meta Validation (5 Checks)**
- Rejects requests missing \`_meta\` or lacking structural required internal subfields (\`protocolVersion\`, \`clientInfo\`, \`clientCapabilities\`) with a JSON-RPC \`-32602 Invalid params\` error signature and an HTTP status code \`400 Bad Request\`.
2. **Discovery & Capabilities (3 Checks)**
- Implements \`server/discover\` mapping exact mandatory protocol elements.
- Dynamically checks prompt capability declaration constraints, validates that active RPC handlers match advertised discovery capacities.
Expand Down Expand Up @@ -356,26 +356,50 @@ export class ServerStatelessScenario implements ClientScenario {
}
];
for (const testCase of metaValidationTestCases) {
const metaProbe = await sendRpc(
'server/discover',
testCase.params,
undefined,
testCase.rpcId
).catch(() => null);
const metaRes: any = metaProbe?.res ?? null;
const metaData: any = metaProbe?.data ?? null;
if (metaData) checkErrorId(metaData, testCase.rpcId);

await runCheck(
`sep-2575-request-meta-invalid-${testCase.slug}`,
'RequestMetaInvalid',
testCase.description,
async () => {
const { data } = await sendRpc(
'server/discover',
testCase.params,
undefined,
testCase.rpcId
);
checkErrorId(data, testCase.rpcId);
() => {
if (!metaProbe)
return { error: '_meta validation probe failed completely' };
if (metaData?.error?.code !== -32602) {
return {
error: `Expected error code -32602, got ${metaData?.error?.code}`,
details: { fieldIssue: testCase.slug, response: metaData }
};
}
return { details: { fieldIssue: testCase.slug, response: metaData } };
},
{ fieldIssue: testCase.slug }
);

if (data?.error?.code !== -32602) {
// Companion HTTP-status check: a request missing a required _meta field
// is malformed and, on HTTP, the rejection MUST use 400 Bad Request.
await runCheck(
'sep-2575-http-server-meta-invalid-400',
'HttpServerMetaInvalid400',
'Rejections of requests missing required _meta fields use HTTP 400 Bad Request.',
() => {
if (!metaRes)
return { error: '_meta validation probe failed completely' };
if (metaRes.status !== 400) {
return {
error: `Expected error code -32602, got ${data?.error?.code}`,
details: { fieldIssue: testCase.slug, response: data }
error: `Expected HTTP 400 Bad Request, got status code ${metaRes.status}`,
details: { fieldIssue: testCase.slug, response: metaData }
};
}
return { details: { fieldIssue: testCase.slug, response: data } };
return { details: { fieldIssue: testCase.slug, response: metaData } };
},
{ fieldIssue: testCase.slug }
);
Expand Down Expand Up @@ -498,14 +522,15 @@ export class ServerStatelessScenario implements ClientScenario {
// ==========================================
// 3. Version Negotiation & Headers (3 Checks)
// ==========================================
const requestedUnsupportedVersion = 'v999.0.0';
const unsupportedMeta = {
...validMeta,
'io.modelcontextprotocol/protocolVersion': 'v999.0.0'
'io.modelcontextprotocol/protocolVersion': requestedUnsupportedVersion
};
const response301 = await sendRpc(
'server/discover',
{ _meta: unsupportedMeta },
{ 'MCP-Protocol-Version': 'v999.0.0' },
{ 'MCP-Protocol-Version': requestedUnsupportedVersion },
301
).catch(() => null);
const res301: any = response301?.res ?? null;
Expand All @@ -515,7 +540,7 @@ export class ServerStatelessScenario implements ClientScenario {
await runCheck(
'sep-2575-server-unsupported-version-error',
'ServerUnsupportedVersionError',
'If the server does not implement the requested version (whether the version is unknown to the server, or is a known version the server has chosen not to support), it MUST respond with an UnsupportedProtocolVersionError listing the versions it does support.',
'If the server does not implement the requested version (whether the version is unknown to the server, or is a known version the server has chosen not to support), it MUST respond with an UnsupportedProtocolVersionError listing the versions it does support; the error data carries the supported versions and echoes the requested version.',
() => {
if (!data301)
return { error: 'Unsupported version invocation failed completely' };
Expand All @@ -533,6 +558,17 @@ export class ServerStatelessScenario implements ClientScenario {
return {
error: `Returned supported versions data layout does not correlate to active server metrics: ${JSON.stringify(errSupportedVersions)}`
};

// UnsupportedProtocolVersionError data carries both required members:
// `supported` (asserted above) and `requested`, which echoes the
// version the request asked for.
const requestedEcho = data301?.error?.data?.requested;
if (requestedEcho !== requestedUnsupportedVersion) {
return {
error: `error.data.requested must echo the requested version '${requestedUnsupportedVersion}', got ${JSON.stringify(requestedEcho)}`,
details: { response: data301 }
};
}
return { details: { response: data301 } };
}
);
Expand Down
Loading