Skip to content

Commit bd84329

Browse files
Doc update + validation in SseServerTransport + existing test fixes: addresses Issue: #827 (#900)
1 parent b16c2a8 commit bd84329

File tree

2 files changed

+55
-2
lines changed

2 files changed

+55
-2
lines changed

src/mcp/server/sse.py

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,14 +80,39 @@ class SseServerTransport:
8080
def __init__(self, endpoint: str, security_settings: TransportSecuritySettings | None = None) -> None:
8181
"""
8282
Creates a new SSE server transport, which will direct the client to POST
83-
messages to the relative or absolute URL given.
83+
messages to the relative path given.
8484
8585
Args:
86-
endpoint: The relative or absolute URL for POST messages.
86+
endpoint: A relative path where messages should be posted
87+
(e.g., "/messages/").
8788
security_settings: Optional security settings for DNS rebinding protection.
89+
90+
Note:
91+
We use relative paths instead of full URLs for several reasons:
92+
1. Security: Prevents cross-origin requests by ensuring clients only connect
93+
to the same origin they established the SSE connection with
94+
2. Flexibility: The server can be mounted at any path without needing to
95+
know its full URL
96+
3. Portability: The same endpoint configuration works across different
97+
environments (development, staging, production)
98+
99+
Raises:
100+
ValueError: If the endpoint is a full URL instead of a relative path
88101
"""
89102

90103
super().__init__()
104+
105+
# Validate that endpoint is a relative path and not a full URL
106+
if "://" in endpoint or endpoint.startswith("//") or "?" in endpoint or "#" in endpoint:
107+
raise ValueError(
108+
f"Given endpoint: {endpoint} is not a relative path (e.g., '/messages/'), \
109+
expecting a relative path(e.g., '/messages/')."
110+
)
111+
112+
# Ensure endpoint starts with a forward slash
113+
if not endpoint.startswith("/"):
114+
endpoint = "/" + endpoint
115+
91116
self._endpoint = endpoint
92117
self._read_stream_writers = {}
93118
self._security = TransportSecurityMiddleware(security_settings)

tests/shared/test_sse.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -473,3 +473,31 @@ def test_sse_message_id_coercion():
473473
json_message = '{"jsonrpc": "2.0", "id": "123", "method": "ping", "params": null}'
474474
msg = types.JSONRPCMessage.model_validate_json(json_message)
475475
assert msg == snapshot(types.JSONRPCMessage(root=types.JSONRPCRequest(method="ping", jsonrpc="2.0", id=123)))
476+
477+
478+
@pytest.mark.parametrize(
479+
"endpoint, expected_result",
480+
[
481+
# Valid endpoints - should normalize and work
482+
("/messages/", "/messages/"),
483+
("messages/", "/messages/"),
484+
("/", "/"),
485+
# Invalid endpoints - should raise ValueError
486+
("http://example.com/messages/", ValueError),
487+
("//example.com/messages/", ValueError),
488+
("ftp://example.com/messages/", ValueError),
489+
("/messages/?param=value", ValueError),
490+
("/messages/#fragment", ValueError),
491+
],
492+
)
493+
def test_sse_server_transport_endpoint_validation(endpoint: str, expected_result: str | type[Exception]):
494+
"""Test that SseServerTransport properly validates and normalizes endpoints."""
495+
if isinstance(expected_result, type) and issubclass(expected_result, Exception):
496+
# Test invalid endpoints that should raise an exception
497+
with pytest.raises(expected_result, match="is not a relative path.*expecting a relative path"):
498+
SseServerTransport(endpoint)
499+
else:
500+
# Test valid endpoints that should normalize correctly
501+
sse = SseServerTransport(endpoint)
502+
assert sse._endpoint == expected_result
503+
assert sse._endpoint.startswith("/")

0 commit comments

Comments
 (0)