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
47 changes: 46 additions & 1 deletion src/scenarios/client/request-metadata.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ import {
} from './auth/test_helpers/testClient';
import { getHandler } from '../../../examples/clients/typescript/everything-client';
import { getScenario } from '../index';
import { DECLARED_CHECK_IDS } from './request-metadata';
import {
DECLARED_CHECK_IDS,
RequestMetadataScenario
} from './request-metadata';

// A bad client that does not send _meta
async function badClient(serverUrl: string) {
Expand Down Expand Up @@ -246,6 +249,48 @@ describe('request-metadata client scenario — client never connects', () => {
});
});

describe('request-metadata client scenario — HTTP handling', () => {
test.each(['GET', 'DELETE'])(
'rejects an empty-body %s request without parsing it as JSON',
async (method) => {
const scenario = new RequestMetadataScenario();
const { serverUrl } = await scenario.start(testScenarioContext());

try {
const response = await fetch(serverUrl, { method });

expect(response.status).toBe(405);
expect(response.headers.get('allow')).toBe('POST');
expect(await response.text()).toBe('Method Not Allowed');
} finally {
await scenario.stop();
}
}
);

test('returns a JSON-RPC parse error for malformed POST bodies', async () => {
const scenario = new RequestMetadataScenario();
const { serverUrl } = await scenario.start(testScenarioContext());

try {
const response = await fetch(serverUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: '{'
});

expect(response.status).toBe(400);
expect(await response.json()).toMatchObject({
jsonrpc: '2.0',
id: null,
error: { code: -32700 }
});
} finally {
await scenario.stop();
}
});
});

describe('request-metadata client scenario — negative tests', () => {
test('client fails when omitting _meta', async () => {
const runner = new InlineClientRunner(badClient);
Expand Down
29 changes: 28 additions & 1 deletion src/scenarios/client/request-metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,12 +129,39 @@ export class RequestMetadataScenario implements Scenario {
req: http.IncomingMessage,
res: http.ServerResponse
): void {
// This scenario only evaluates the metadata attached to POST requests.
// Clients may probe a Streamable HTTP endpoint with an empty-body GET
// before sending MCP requests, so reject unsupported methods before
// attempting to parse a JSON-RPC body.
if (req.method !== 'POST') {
res.writeHead(405, { Allow: 'POST' });
res.end('Method Not Allowed');
return;
}

let body = '';
req.on('data', (chunk) => {
body += chunk.toString();
});
req.on('end', () => {
const request = JSON.parse(body);
let request;
try {
request = JSON.parse(body);
} catch (error) {
res.writeHead(400, { 'Content-Type': 'application/json' });
res.end(
JSON.stringify({
jsonrpc: '2.0',
id: null,
error: {
code: -32700,
message: `Parse error: ${String(error)}`
}
})
);
return;
}

this.requestsObserved++;

// Extract version and headers
Expand Down
Loading