Skip to content

fix: resolve URL path truncation in SSE transport for proxied servers #1211

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: main
Choose a base branch
from

Conversation

SOURABHMISHRA5221
Copy link

@SOURABHMISHRA5221 SOURABHMISHRA5221 commented Jul 28, 2025

Summary

Fix SSE endpoint path handling to avoid urllib.parse.urljoin() truncating base paths when servers are behind proxies or mounted at subpaths. Endpoints are treated as relative path segments and stored verbatim.

Motivation

When clients join a base URL with an endpoint starting with “/”, urljoin resets to the host root. This caused paths like http://localhost:8000/some/path/to/sse + /messages/http://localhost:8000/messages/. Using a relative segment preserves the base path. Resolves #200.

from urllib.parse import urljoin
urljoin("http://host/hello/world", "/messages/")  # -> http://host/messages/
urljoin("http://host/hello/world/", "messages/")  # -> http://host/hello/world/messages/

What changed

  • mcp/server/sse.py
    • Store endpoint verbatim (no auto-leading “/”).
    • Validate endpoint is a relative path (no scheme/host/query/fragment).
    • Add _build_message_path(root_path) to construct the final path robustly from the app mount and root_path.
    • Update comments/docstrings to emphasize relative path semantics and predictable URL construction.
  • Tests
    • tests/server/test_sse_security.py: clarify accept/reject cases and document urljoin behavior.
    • tests/shared/test_sse.py: add/clarify param cases; ensure endpoints are stored verbatim.

Backward compatibility

  • No breaking changes.

How it was tested

  • Endpoint validation tests for relative vs absolute and query/fragment rejection.
  • Subpath mounting tests to reproduce and prevent original truncation.
  • All existing SSE security/functionality tests pass.
  • Verified path construction under various root_path combinations.

Checklist

  • Follows repo style guidelines
  • Tests added/updated and passing
  • Error handling and docs updated

@SOURABHMISHRA5221 SOURABHMISHRA5221 requested review from a team as code owners July 28, 2025 19:37
@SOURABHMISHRA5221
Copy link
Author

Hi @Kludex!... Can you please review this...

Comment on lines 42 to 43
- With root_path="/api" and endpoint="/messages/": Final path = "/api/messages/"
- With root_path="/api" and endpoint="messages/": Final path = "/api/messages/"

Choose a reason for hiding this comment

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

The behavior I saw was: urllib.join("http://example.com/some/path", "/messages/") resulted in http://example.com/messages. I think it would be sufficient to just drop the leading forward slash sense that leads to an unexpected behavior. I would argue that the sdk should not be forcing its route to be at any particular location and that a dev should decide where it gets mounted. Relative path joining should allow that. See comment below for test cases

Copy link
Author

Choose a reason for hiding this comment

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

Hi @mconflitti-pbc! Thanks for reviewing this PR, and apologies for the late reply.

You’re right about the behavior: auto-prepending “/” to “make it relative” actually made the endpoint origin-absolute, which caused urljoin to drop any base path in proxied/subpath setups. This change removes that normalization and stores the endpoint verbatim.

  • We validate only that the endpoint is a relative path (no scheme/host/query/fragment).
  • Both “/messages/” and “messages/” are accepted; we recommend the relative segment (“messages/”) for predictable joining.
  • The final URL is composed from the app’s mount and ASGI root_path, so mounting remains entirely up to the developer.

Thanks again for the thoughtful feedback.

Comment on lines 313 to 318
valid_endpoints_and_expected = [
("/messages/", "/messages/"), # Absolute path format
("messages/", "messages/"), # Relative path format
("/api/v1/messages/", "/api/v1/messages/"),
("api/v1/messages/", "api/v1/messages/"),
]

Choose a reason for hiding this comment

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

Here is the behavior demonstrated with examples:

>>> from urllib.parse import urljoin
>>> 
>>> urljoin("http://www.google.com/hello/world", "/messages")
'http://www.google.com/messages'
>>> urljoin("http://www.google.com/hello/world", "messages")
'http://www.google.com/hello/messages'
>>> urljoin("http://www.google.com/hello/world/", "messages")
'http://www.google.com/hello/world/messages'
>>> urljoin("http://www.google.com/hello/world/", "/messages")
'http://www.google.com/messages'
>>> urljoin("http://www.google.com/hello/world/", "/messages/")
'http://www.google.com/messages/'
>>> urljoin("http://www.google.com/hello/world/", "messages/")
'http://www.google.com/hello/world/messages/'
>>> urljoin("http://www.google.com/hello/world", "messages/")
'http://www.google.com/hello/messages/'

urllib has some odd behavior imo.

Choose a reason for hiding this comment

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

I believe this is the one we want to coerce/enforce:

>>> urljoin("http://www.google.com/hello/world/", "messages/")
'http://www.google.com/hello/world/messages/'

Copy link
Author

Choose a reason for hiding this comment

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

I’ve made the necessary updates based on your feedback (store endpoint verbatim, remove leading “/” coercion, clarify docs/tests). When you have a moment, could you please take another look? Thank you

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.

Server url truncated by urllib in FastMCP
2 participants