-
Notifications
You must be signed in to change notification settings - Fork 2.3k
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
base: main
Are you sure you want to change the base?
fix: resolve URL path truncation in SSE transport for proxied servers #1211
Conversation
Hi @Kludex!... Can you please review this... |
src/mcp/server/sse.py
Outdated
- With root_path="/api" and endpoint="/messages/": Final path = "/api/messages/" | ||
- With root_path="/api" and endpoint="messages/": Final path = "/api/messages/" |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
tests/server/test_sse_security.py
Outdated
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/"), | ||
] |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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/'
There was a problem hiding this comment.
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
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 likehttp://localhost:8000/some/path/to/sse
+/messages/
→http://localhost:8000/messages/
. Using a relative segment preserves the base path. Resolves #200.What changed
mcp/server/sse.py
endpoint
verbatim (no auto-leading “/”)._build_message_path(root_path)
to construct the final path robustly from the app mount androot_path
.tests/server/test_sse_security.py
: clarify accept/reject cases and documenturljoin
behavior.tests/shared/test_sse.py
: add/clarify param cases; ensure endpoints are stored verbatim.Backward compatibility
How it was tested
root_path
combinations.Checklist