Skip to content

fix(api): return 405 for GET/DELETE on MCP endpoint#2691

Open
jordan-simonovski wants to merge 3 commits into
mainfrom
jordansimonovski/mcp-transport-405
Open

fix(api): return 405 for GET/DELETE on MCP endpoint#2691
jordan-simonovski wants to merge 3 commits into
mainfrom
jordansimonovski/mcp-transport-405

Conversation

@jordan-simonovski

@jordan-simonovski jordan-simonovski commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Fixes #2686.
resolves HDX-4843

The MCP Streamable HTTP endpoint (/api/mcp) rejects spec-compliant clients: GET and DELETE return statuses that make official MCP SDK clients (e.g. Claude Code) abort the connection, so the server is unusable with them even though POST-based initialize/tools/list/tools/call all work. This makes GET and DELETE return 405 Method Not Allowed, which those clients treat as "not offered, continue".

What changed

  • GET /api/mcp and DELETE /api/mcp now return 405 with an Allow: POST header.
  • POST handling is unchanged; rate limiting still applies to every method.

Background

The transport is stateless — a fresh MCP server/transport is created per POST and torn down immediately after. Such a server offers neither a server-initiated SSE stream (GET) nor client-terminable sessions (DELETE), and the Streamable HTTP spec requires 405 in exactly that case. Previously app.all('/') routed every method through the SDK, which opened a doomed SSE stream on GET (closed instantly by the per-request teardown) and answered DELETE with a non-405 status — both of which SDK clients treat as a failed connection.

The issue also reports a third symptom: notifications/initialized sent without a params member returning 400. That was fixed by the MCP SDK itself — the version this repo now depends on treats params as optional in the notification schema, so no code change is needed here. It was only reproducible on the older SDK shipped in 2.31.0.

Impact

Spec-compliant MCP clients can now complete the handshake and connect without a reverse-proxy shim. No change for existing POST-based usage.

Implementation detail
  • A shared methodNotAllowed handler is registered for GET and DELETE; POST keeps the full transport pipeline.
  • Added packages/api/src/mcp/__tests__/app.test.ts — supertest checks asserting 405 + Allow: POST for both methods.
  • PUT/PATCH now fall through to Express's default 404 rather than the SDK's 405. This is immaterial in practice — MCP clients only use GET/POST/DELETE — and the meaningful transport methods are handled explicitly.

The stateless Streamable HTTP transport routed every method through the SDK,
which answered GET with a server-initiated SSE stream that the per-request
teardown immediately closes, and DELETE with a non-405 status. MCP SDK clients
(e.g. Claude Code) abort the connection on anything but 405, so they could not
connect at all.

GET and DELETE now return 405 Method Not Allowed with Allow: POST, which the
spec requires for a server that offers neither a standalone SSE stream nor
client-terminable sessions. POST is unchanged. Rate limiting is kept on all
methods.
@vercel

vercel Bot commented Jul 21, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
hyperdx-oss Ready Ready Preview, Comment Jul 21, 2026 1:21am
hyperdx-storybook Ready Ready Preview, Comment Jul 21, 2026 1:21am

Request Review

@changeset-bot

changeset-bot Bot commented Jul 21, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: b5c5ad7

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 3 packages
Name Type
@hyperdx/api Patch
@hyperdx/app Patch
@hyperdx/otel-collector Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions github-actions Bot added the review/tier-2 Low risk — AI review + quick human skim label Jul 21, 2026
@github-actions

github-actions Bot commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

🔵 Tier 2 — Low Risk

Small, isolated change with no API route or data model modifications.

Why this tier:

  • Standard feature/fix — introduces new logic or modifies core functionality

Review process: AI review + quick human skim (target: 5–15 min). Reviewer validates AI assessment and checks for domain-specific concerns.
SLA: Resolve within 4 business hours.

Stats
  • Production files changed: 1
  • Production lines changed: 19 (+ 24 in test files, excluded from tier calculation)
  • Branch: jordansimonovski/mcp-transport-405
  • Author: jordan-simonovski

To override this classification, remove the review/tier-2 label and apply a different review/tier-* label. Manual overrides are preserved on subsequent pushes.

@greptile-apps

greptile-apps Bot commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR updates the MCP HTTP endpoint to make unsupported methods explicit. The main changes are:

  • GET and DELETE now return 405 with Allow: POST.
  • POST keeps the existing MCP transport path.
  • Unit tests cover the new GET and DELETE responses.
  • A patch changeset documents the behavior change.

Confidence Score: 4/5

This is close, but the method discovery behavior should be fixed before merging.

  • GET and DELETE now follow the intended 405 behavior.
  • The router can still expose an inconsistent OPTIONS response when the request reaches the MCP sub-app directly.
  • The new tests do not cover that method-discovery path.

packages/api/src/mcp/app.ts

Important Files Changed

Filename Overview
packages/api/src/mcp/app.ts Adds explicit GET and DELETE 405 handlers, but omits a router-level OPTIONS fallback.
packages/api/src/mcp/tests/app.test.ts Adds focused tests for GET and DELETE responses.
.changeset/mcp-transport-405.md Adds a patch changeset for the MCP transport response change.

Fix All in Claude Code Fix All in Conductor Fix All in Cursor Fix All in Codex

Reviews (3): Last reviewed commit: "fix(api): leave MCP OPTIONS to the globa..." | Re-trigger Greptile

Comment thread packages/api/src/mcp/app.ts
Express's automatic OPTIONS handler builds its Allow header from every
registered route, which advertised GET and DELETE as usable even though both
return 405. Route OPTIONS through the same handler so the endpoint honestly
reports POST as its only supported method. The endpoint sets no CORS headers,
so it was never a working preflight target.
@github-actions

github-actions Bot commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

E2E Test Results

All tests passed • 240 passed • 1 skipped • 814s

Status Count
✅ Passed 240
❌ Failed 0
⚠️ Flaky 2
⏭️ Skipped 1

Tests ran across 4 shards in parallel.

View full report →

Comment thread packages/api/src/mcp/app.ts Outdated
@github-actions

github-actions Bot commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Deep Review

No critical issues found. The change is correct: GET/DELETE on the stateless MCP transport now return 405 with Allow: POST, which is the spec-correct behavior, and the POST path is untouched. Security, correctness, and reliability reviewers found no defects — the 405 handler is synchronous, allocates nothing, stays behind the rate limiter, and leaks no information. The recommendations below are hardening and hygiene only.

🟡 P2 -- recommended

  • packages/api/src/mcp/app.ts:33 -- The router deliberately delegates OPTIONS to the upstream global CORS middleware instead of answering 405, but no test locks in that invariant, so a later app.options handler or a change in CORS mount ordering could silently break browser preflight.
    • Fix: Add a router-level test asserting OPTIONS /mcp does not return 405, plus an integration test that mounts the real CORS middleware and asserts the 204 preflight response.
    • testing, maintainability, api-contract
🔵 P3 nitpicks (2)
  • packages/api/src/mcp/app.ts:34 -- The 405 response sets Allow: POST, but the resource also answers OPTIONS (204 via CORS), so per RFC 7231 §7.4.1 the header under-reports the methods the resource supports.
    • Fix: Set the header to Allow: POST, OPTIONS.
  • packages/api/src/mcp/app.ts:36 -- Replacing the catch-all route with explicit GET/DELETE/POST handlers routes PUT and PATCH to Express's default 404 rather than a 405, an inconsistency with the GET/DELETE handling in the same file (no MCP client sends these, so impact is negligible).
    • Fix: Route the remaining unsupported methods through methodNotAllowed (e.g. a trailing app.all('/', methodNotAllowed)) so every unsupported method returns a uniform 405 with Allow.
    • correctness, api-contract

Reviewers (6): correctness, security, testing, maintainability, reliability, api-contract.

Testing gaps:

  • The POST sad-path branches returning 403 (missing teamId, missing userId) are untested despite being trivially mockable.
  • The POST happy path is untested; excludable here since it requires a real transport and an authenticated user.
  • No end-to-end assertion that OPTIONS /mcp returns 204 through the real CORS stack.

Note: git/bash were unavailable in this environment (a bwrap sandbox failure blocked every shell command), so reviewers worked from the full file contents supplied inline plus the Read tool rather than git diff. createMcpExpressApp() SDK internals could not be inspected; if it pre-registers its own / routes, handler-shadowing is theoretically possible but the passing GET/DELETE tests indicate it does not.

The previous commit's per-route OPTIONS handler was dead code: the global CORS
middleware (api-app.ts) short-circuits every OPTIONS preflight with a 204
before the /mcp router runs, so the handler never executed in production and
only 'worked' in the isolated unit test. It was also semantically wrong —
returning 405 to a CORS preflight would break browser preflight. Revert it and
document why OPTIONS is left to the CORS layer. GET/DELETE are unaffected: CORS
only short-circuits OPTIONS, so they still reach the 405 handler.
Comment thread packages/api/src/mcp/app.ts
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

review/tier-2 Low risk — AI review + quick human skim

Projects

None yet

Development

Successfully merging this pull request may close these issues.

MCP endpoint (/api/mcp) rejects spec-compliant clients — 400 on GET, DELETE, and notifications without params. 405

1 participant