22SSE Server Transport Module
33
44This module implements a Server-Sent Events (SSE) transport layer for MCP servers.
5- Fixes the URL path joining issue when using subpaths/proxied servers.
5+ Endpoints are specified as relative paths. This aligns with common client URL
6+ construction patterns (for example, `urllib.parse.urljoin`) and works correctly
7+ when applications are deployed behind proxies or at subpaths.
68
79Example usage:
810```python
9- # Option 1: Create an SSE transport with absolute path (leading slash)
10- # This treats "/messages/" as absolute within the app
11- sse = SseServerTransport("/messages/")
12-
13- # Option 2: Create an SSE transport with relative path (no leading slash)
14- # This treats "messages/" as relative to the root path - RECOMMENDED for proxied servers
11+ # Recommended: provide a relative path segment (no scheme/host/query/fragment).
12+ # Using "messages/" works well with clients that build absolute URLs using
13+ # `urllib.parse.urljoin`, including in proxied/subpath deployments.
1514 sse = SseServerTransport("messages/")
1615
1716 # Create Starlette routes for SSE and message handling
@@ -36,14 +35,16 @@ async def handle_sse(request):
3635 uvicorn.run(starlette_app, host="127.0.0.1", port=port)
3736```
3837
39- Path behavior examples:
40- - With root_path="" and endpoint="/messages/": Final path = "/messages/"
41- - With root_path="" and endpoint="messages/": Final path = "/messages/"
42- - With root_path="/api" and endpoint="/messages/": Final path = "/api/messages/"
43- - With root_path="/api" and endpoint="messages/": Final path = "/api/messages/"
38+ Path behavior examples inside the server (final path emitted to clients):
39+ - root_path="" and endpoint="messages/" -> "/messages/"
40+ - root_path="/api" and endpoint="messages/" -> "/api/messages/"
41+
42+ Note: When clients use `urllib.parse.urljoin(base, path)`, joining a segment that
43+ starts with "/" replaces the base path. Providing a relative segment like
44+ `"messages/?id=1"` preserves the base path as intended.
4445
45- For servers behind proxies or mounted at subpaths, use the relative path format
46- (without leading slash) to ensure proper URL joining with urllib.parse. urljoin() .
46+ For servers behind proxies or mounted at subpaths, prefer a relative path without
47+ leading slash (e.g., "messages/" ) to ensure correct joining with ` urljoin` .
4748
4849Note: The handle_sse function must return a Response to avoid a "TypeError: 'NoneType'
4950object is not callable" error when client disconnects. The example above returns
@@ -98,8 +99,10 @@ def __init__(self, endpoint: str, security_settings: TransportSecuritySettings |
9899 messages to the relative path given.
99100
100101 Args:
101- endpoint: A relative path where messages should be posted
102- (e.g., "/messages/" or "messages/").
102+ endpoint: Relative path segment where messages should be posted
103+ (e.g., "messages/"). Avoid scheme/host/query/fragment. When
104+ clients construct absolute URLs using `urllib.parse.urljoin`,
105+ relative segments preserve any existing base path.
103106 security_settings: Optional security settings for DNS rebinding protection.
104107
105108 Note:
@@ -111,25 +114,24 @@ def __init__(self, endpoint: str, security_settings: TransportSecuritySettings |
111114 3. Portability: The same endpoint configuration works across different
112115 environments (development, staging, production)
113116
114- The endpoint path handling has been updated to work correctly with urllib.parse.urljoin()
115- when servers are behind proxies or mounted at subpaths.
117+ The endpoint path handling preserves the provided relative path and is
118+ suitable for deployments under proxies or subpaths.
116119
117120 Raises:
118121 ValueError: If the endpoint is a full URL instead of a relative path
119122 """
120123
121124 super ().__init__ ()
122125
123- # Validate that endpoint is a relative path and not a full URL
126+ # Validate that endpoint is a relative path and not a full URL.
124127 if "://" in endpoint or endpoint .startswith ("//" ) or "?" in endpoint or "#" in endpoint :
125128 raise ValueError (
126- f"Given endpoint: { endpoint } is not a relative path (e.g., '/messages/' or ' messages/'), "
127- "expecting a relative path (e.g., '/messages/' or 'messages/') ."
129+ f"Given endpoint: { endpoint } is not a relative path (e.g., 'messages/'), "
130+ "expecting a relative path with no scheme/host/query/fragment ."
128131 )
129132
130- # Handle leading slash more intelligently
131- # Remove automatic leading slash enforcement to support proper URL joining
132- # Store the endpoint as-is, allowing both "/messages/" and "messages/" formats
133+ # Store the endpoint as provided to retain relative-path semantics and make
134+ # client URL construction predictable across deployment topologies.
133135 self ._endpoint = endpoint
134136 self ._read_stream_writers = {}
135137 self ._security = TransportSecurityMiddleware (security_settings )
@@ -139,8 +141,9 @@ def _build_message_path(self, root_path: str) -> str:
139141 """
140142 Helper method to properly construct the message path
141143
142- This method handles the path construction logic that was causing issues
143- with urllib.parse.urljoin() when servers are proxied or mounted at subpaths.
144+ Constructs the message path relative to the app's mount point and the
145+ provided `root_path`. The stored endpoint is treated as path-absolute if
146+ it starts with "/", otherwise as a relative segment.
144147
145148 Args:
146149 root_path: The root path from ASGI scope (e.g., "" or "/api_prefix")
@@ -151,10 +154,10 @@ def _build_message_path(self, root_path: str) -> str:
151154 # Clean up the root path
152155 clean_root_path = root_path .rstrip ("/" )
153156
154- # If endpoint starts with "/", it's meant to be absolute within the app
155- # If endpoint doesn't start with "/", it's meant to be relative to root_path
157+ # If endpoint starts with "/", treat it as path- absolute from the app mount;
158+ # otherwise, treat it as relative to ` root_path`.
156159 if self ._endpoint .startswith ("/" ):
157- # Absolute path within the app - just concatenate
160+ # Path-absolute within the app mount - just concatenate
158161 full_path = clean_root_path + self ._endpoint
159162 else :
160163 # Relative path - ensure proper joining
0 commit comments