Skip to content

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

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

fix(api): return 405 for GET/DELETE on MCP endpoint#2691
jordan-simonovski wants to merge 4 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 23, 2026 11:31pm
hyperdx-storybook Ready Ready Preview, Comment Jul 23, 2026 11:31pm

Request Review

@changeset-bot

changeset-bot Bot commented Jul 21, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 16f5beb

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 transport behavior for unsupported methods. The main changes are:

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

Confidence Score: 5/5

This looks safe to merge.

  • No blocking issues found in the changed code.

Important Files Changed

Filename Overview
packages/api/src/mcp/app.ts Adds explicit GET and DELETE handlers that return 405 while keeping POST on the authenticated MCP transport path.
packages/api/src/mcp/tests/app.test.ts Adds unit coverage for GET and DELETE returning 405 with Allow: POST.
.changeset/mcp-transport-405.md Adds a patch changeset for the MCP transport response change.

Reviews (4): Last reviewed commit: "Merge branch 'main' into jordansimonovsk..." | 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 • 241 passed • 1 skipped • 1017s

Status Count
✅ Passed 241
❌ 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

⚠️ The review could not be completed in this environment.

Every shell command failed at the sandbox layer with bwrap: Can't create file at /home/.mcp.json: Permission denied — including plain echo, and including attempts with the sandbox explicitly disabled. As a result git diff, git log, and the reviewer sub-agent tooling could not execute, so the actual PR diff against c4a223302156d2db5ffcf1900830414de01fa168 was never obtainable. File-tool fallbacks (Glob/Grep) were also unavailable, and directory reads are not supported.

No findings are reported below because none could be verified against the real diff. This is an absence of analysis, not a clean bill of health — please do not read it as an approval. Emitting findings assembled only from the PR description would risk seeding the downstream resolver with unverified, author-derived text, which the review contract explicitly prohibits.

Suggested next step: re-run this review in an environment where the shell and git are functional so the reviewer sub-agents can read the actual diff and produce confidence-gated findings.


Reviewers (0): none — sub-agent dispatch was blocked by the shell/sandbox failure before any reviewer could read the diff.

Testing gaps: not assessed — the new packages/api/src/mcp/__tests__/app.test.ts and the endpoint changes could not be read.

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

2 participants