Skip to content

fix: handle /mcp without trailing slash by adding explicit route - #27167

Closed
krrish-berri-2 wants to merge 1 commit into
litellm_internal_stagingfrom
cursor/fix-mcp-trailing-slash-d804
Closed

fix: handle /mcp without trailing slash by adding explicit route#27167
krrish-berri-2 wants to merge 1 commit into
litellm_internal_stagingfrom
cursor/fix-mcp-trailing-slash-d804

Conversation

@krrish-berri-2

Copy link
Copy Markdown
Contributor

Relevant issues

Fixes the issue where /mcp (without trailing slash) doesn't work but /mcp/ does.

Pre-Submission checklist

  • I have Added testing in the tests/test_litellm/ directory, Adding at least 1 test is a hard requirement - see details
  • My PR passes all unit tests on make test-unit
  • My PR's scope is as isolated as possible, it only solves 1 specific problem

Type

🐛 Bug Fix

Changes

When the MCP sub-app is mounted via app.mount("/mcp", mcp_app), Starlette issues a 307 redirect from /mcp to /mcp/. This drops the request body and breaks MCP clients that don't follow redirects.

Fix: Added an explicit @app.api_route("/mcp", ...) on the parent FastAPI app that forwards requests directly to the MCP streamable-HTTP handler via the existing _stream_mcp_asgi_response helper. This ensures both /mcp and /mcp/ work identically without any redirect.

Files changed:

  • litellm/proxy/proxy_server.py — Added bare_mcp_route handler before the toolset/dynamic MCP routes
  • tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_bare_route.py — Unit test verifying the explicit /mcp route exists on the parent app with correct HTTP methods

Slack Thread

Open in Web Open in Cursor 

Starlette's Mount('/mcp', ...) issues a 307 redirect from /mcp to /mcp/,
which drops the request body and breaks MCP clients. Add an explicit
@app.api_route('/mcp') on the parent app that forwards directly to the
MCP streamable-HTTP handler, so both /mcp and /mcp/ work identically.

Co-authored-by: Krrish Dholakia <krrish-berri-2@users.noreply.github.com>
@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

@codecov

codecov Bot commented May 5, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@krrish-berri-2
krrish-berri-2 marked this pull request as ready for review May 5, 2026 17:23
@krrish-berri-2
krrish-berri-2 enabled auto-merge (squash) May 5, 2026 17:23
@krrish-berri-2

Copy link
Copy Markdown
Contributor Author

@greptile

@greptile-apps

greptile-apps Bot commented May 5, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes the 307-redirect loop that occurs when Starlette's Mount(\"/mcp\", mcp_app) receives a request to /mcp (without trailing slash): it adds an explicit @app.api_route(\"/mcp\", ...) on the parent FastAPI app that forwards directly to handle_streamable_http_mcp, exactly mirroring the pattern already used for the toolset and dynamic MCP routes.

  • proxy_server.py: New bare_mcp_route registered before the lazy-loaded Mount(\"/mcp\", mcp_app), so it wins on exact /mcp matches; auth is handled inside handle_streamable_http_mcp (via extract_mcp_auth_context) just as the other proxy-level MCP routes do.
  • test_mcp_bare_route.py: Structural test confirming the route exists in app.routes with at least GET and POST methods; no real network calls.

Confidence Score: 4/5

Safe to merge; the change adds one small forwarding route with no effect on existing /mcp/ traffic or any other endpoint.

The implementation is correct and consistent with the existing toolset/dynamic MCP route pattern. The only findings are a redundant scope path assignment and minor test coverage gaps.

No files require special attention; both changed files are straightforward.

Important Files Changed

Filename Overview
litellm/proxy/proxy_server.py Adds bare_mcp_route — an explicit @app.api_route("/mcp", ...) that forwards directly to handle_streamable_http_mcp, preventing Starlette's Mount 307 redirect; consistent with existing toolset/dynamic MCP route patterns; a redundant scope["path"] = "/mcp" assignment is present but harmless.
tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_bare_route.py Structural unit test verifying the /mcp route exists on the parent app; no network calls made; has an unused pytest import and only spot-checks GET/POST rather than asserting the full set of seven registered HTTP methods.

Reviews (1): Last reviewed commit: "fix: handle /mcp without trailing slash ..." | Re-trigger Greptile

Comment on lines +7 to +8
import pytest
from starlette.routing import Route

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 The pytest import is unused — no @pytest.mark decorators or fixtures are referenced anywhere in this test file.

Suggested change
import pytest
from starlette.routing import Route
from starlette.routing import Route

Comment on lines +26 to +28
route = mcp_routes[0]
assert "GET" in route.methods
assert "POST" in route.methods

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 The assertion only verifies GET and POST, but the route is registered with seven methods: GET, POST, PUT, DELETE, PATCH, OPTIONS, and HEAD. A missing method here would go undetected — for example, a copy-paste error that accidentally dropped DELETE would not fail this test.

Suggested change
route = mcp_routes[0]
assert "GET" in route.methods
assert "POST" in route.methods
route = mcp_routes[0]
assert route.methods == {"GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD"}

Comment on lines +14976 to +14978
scope = dict(request.scope)
scope["path"] = "/mcp"
return await _stream_mcp_asgi_response(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 The scope["path"] = "/mcp" assignment is redundant: any request routed to bare_mcp_route already has a scope path of /mcp. Removing it makes the intent of the copied scope clearer and aligns with the fact that no path transformation is happening here (unlike the toolset route, which translates /toolset/<name>/mcp/mcp).

Suggested change
scope = dict(request.scope)
scope["path"] = "/mcp"
return await _stream_mcp_asgi_response(
scope = dict(request.scope)
return await _stream_mcp_asgi_response(

@oss-pr-review-agent-shin

Copy link
Copy Markdown
Contributor

🤖 litellm-agent: Merged into staging branch litellm_agent_oss_staging_05_05_2026. Staging PR: #27162

auto-merge was automatically disabled May 5, 2026 20:22

Pull request was closed

oss-pr-review-agent-shin Bot added a commit that referenced this pull request May 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants