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
5 changes: 5 additions & 0 deletions .changeset/stateless-405-allow-header.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@modelcontextprotocol/server': patch
---

`createMcpHandler` now sends `Allow: POST` on every `405` it answers to a non-POST request, with `legacy: 'stateless'` (the default) and with `legacy: 'reject'`.
3 changes: 2 additions & 1 deletion packages/server/src/server/createMcpHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,8 @@ function jsonRpcErrorResponse(httpStatus: number, code: number, message: string,
error: { code, message, ...(data !== undefined && { data }) },
id
},
{ status: httpStatus }
// This entry serves POST only, so every 405 it answers names POST (RFC 9110 §15.5.6).
{ status: httpStatus, ...(httpStatus === 405 && { headers: { Allow: 'POST' } }) }
);
}

Expand Down
1 change: 1 addition & 0 deletions packages/server/test/server/createMcpHandler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -546,6 +546,7 @@ describe('createMcpHandler — stateless legacy fallback (the default)', () => {
for (const method of ['GET', 'DELETE']) {
const response = await handler.fetch(new Request('http://localhost/mcp', { method }));
expect(response.status).toBe(405);
expect(response.headers.get('Allow')).toBe('POST');
const body = (await response.json()) as JSONRPCErrorBody;
expect(body.error.code).toBe(-32_000);
expect(body.error.message).toBe('Method not allowed.');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ describe('legacyStatelessFallback', () => {
for (const method of ['GET', 'DELETE']) {
const response = await handler(new Request('http://localhost/mcp', { method }));
expect(response.status).toBe(405);
expect(response.headers.get('Allow')).toBe('POST');
const body = (await response.json()) as JSONRPCErrorBody;
expect(body.error.code).toBe(-32_000);
expect(body.error.message).toBe('Method not allowed.');
Expand Down
4 changes: 2 additions & 2 deletions test/e2e/requirements.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2755,9 +2755,9 @@ export const REQUIREMENTS: Record<string, Requirement> = {
'typescript:hosting:entry:method-405': {
source: 'sdk',
behavior:
'A non-POST HTTP method (GET, DELETE, PUT, PATCH) on a createMcpHandler endpoint is answered 405 with a JSON-RPC Method-not-allowed body on both legs: the stateless legacy fallback rejects every non-POST method, and the modern-only strict path rejects body-less non-POST traffic via the modern-only-method-not-allowed cell.',
'A non-POST HTTP method (GET, DELETE, PUT, PATCH) on a createMcpHandler endpoint is answered 405 with `Allow: POST` and a JSON-RPC Method-not-allowed body on both legs: the stateless legacy fallback rejects every non-POST method, and the modern-only strict path rejects body-less non-POST traffic via the modern-only-method-not-allowed cell.',
transports: ['entryStateless', 'entryModern'],
note: 'Runs on the createMcpHandler entry arms; each non-POST method is sent through wired.fetch so the HTTP status and body are observed directly. The entry does not emit an Allow header (the per-session server transport does), so only the status and JSON-RPC error shape are pinned.'
note: 'Runs on the createMcpHandler entry arms; each non-POST method is sent through wired.fetch so the HTTP status and body are observed directly. The status, the `Allow` header and the JSON-RPC error shape are pinned.'
},
'typescript:hosting:entry:parse-error-400': {
source: 'https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#sending-messages-to-the-server',
Expand Down
1 change: 1 addition & 0 deletions test/e2e/scenarios/hosting-entry-http.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ verifies('typescript:hosting:entry:method-405', async ({ transport }: TestArgs)
for (const method of ['GET', 'DELETE', 'PUT', 'PATCH']) {
const response = await wired.fetch!(wired.url!, { method });
expect(response.status).toBe(405);
expect(response.headers.get('allow')).toBe('POST');
const body = (await response.json()) as { jsonrpc: string; error: { code: number; message: string } };
expect(body.jsonrpc).toBe('2.0');
expect(body.error.code).toBe(-32_000);
Expand Down
Loading