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 3 commits into
base: main
Choose a base branch
from

Conversation

SOURABHMISHRA5221
Copy link

Summary

Fixed URL path truncation issue in SSE server transport when using urllib.parse.urljoin() with servers behind proxies or mounted at subpaths.

Motivation and Context

This change fixes issue #200 where urllib.parse.urljoin() was truncating server URL paths when combining base URLs with endpoints that have leading slashes. For example, joining http://localhost:8000/some/path/to/sse with /messages/ would incorrectly output http://localhost:8000/messages/ instead of http://localhost:8000/some/path/to/sse/messages/. This was particularly problematic for servers behind proxies or mounted at subpaths.

How Has This Been Tested?

  • Added comprehensive test coverage for both absolute (/messages/) and relative (messages/) endpoint formats
  • Added specific test for subpath mounting scenarios that reproduce the original issue
  • Added endpoint validation tests to ensure proper rejection of invalid URLs
  • All existing security and functionality tests continue to pass
  • Tested path construction behavior with various root_path combinations

Breaking Changes

No breaking changes. The fix maintains backward compatibility:

  • Existing code using SseServerTransport("/messages/") continues to work unchanged
  • New relative format SseServerTransport("messages/") is now recommended for proxied servers
  • Both formats produce identical functional behavior

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have added or updated documentation as needed

Additional context

Key changes made:

  1. Removed automatic leading slash enforcement - allows both /messages/ and messages/ formats
  2. Added _build_message_path() helper method - intelligently constructs paths based on endpoint format
  3. Updated documentation and examples - shows both usage patterns with clear recommendations

Technical details:

  • The issue occurred because urllib.parse.urljoin("http://host/path", "/messages/") treats /messages/ as an absolute path, replacing the entire path portion
  • The fix uses relative paths (messages/) when possible, which get properly appended: urllib.parse.urljoin("http://host/path/", "messages/")http://host/path/messages/
  • Path construction logic handles both formats correctly, ensuring consistent behavior regardless of which format is used

Resolves: #200

@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

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/'

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.

2 participants