From 8dacf70ae0e276281ddd0e11cac1ad0716c18380 Mon Sep 17 00:00:00 2001 From: Thomas Sarlandie Date: Tue, 21 Jul 2026 23:29:09 +0000 Subject: [PATCH] fix request metadata HTTP method handling --- src/scenarios/client/request-metadata.test.ts | 47 ++++++++++++++++++- src/scenarios/client/request-metadata.ts | 29 +++++++++++- 2 files changed, 74 insertions(+), 2 deletions(-) diff --git a/src/scenarios/client/request-metadata.test.ts b/src/scenarios/client/request-metadata.test.ts index 567d1caf..0227d3f8 100644 --- a/src/scenarios/client/request-metadata.test.ts +++ b/src/scenarios/client/request-metadata.test.ts @@ -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) { @@ -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); diff --git a/src/scenarios/client/request-metadata.ts b/src/scenarios/client/request-metadata.ts index 2d1d5dc2..54673b00 100644 --- a/src/scenarios/client/request-metadata.ts +++ b/src/scenarios/client/request-metadata.ts @@ -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