From 8fa013c7e3dcf0f34092ddc5155665ab62164263 Mon Sep 17 00:00:00 2001 From: Aishwarya Date: Sat, 7 Jun 2025 10:53:26 +0545 Subject: [PATCH 1/5] Remove misleading comment and add details on why the current design choice was made. Realted Issue: https://github.com/modelcontextprotocol/python-sdk/issues/827 --- src/mcp/server/sse.py | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/src/mcp/server/sse.py b/src/mcp/server/sse.py index 192c1290b..62c5e293d 100644 --- a/src/mcp/server/sse.py +++ b/src/mcp/server/sse.py @@ -75,10 +75,36 @@ class SseServerTransport: def __init__(self, endpoint: str) -> None: """ Creates a new SSE server transport, which will direct the client to POST - messages to the relative or absolute URL given. + messages to the relative path given. + + Args: + endpoint: A relative path where messages should be posted (e.g., "/messages/") + + Note: + We use relative paths instead of full URLs for several reasons: + 1. Security: Prevents cross-origin requests by ensuring clients only connect + to the same origin they established the SSE connection with + 2. Flexibility: The server can be mounted at any path without needing to + know its full URL + 3. Portability: The same endpoint configuration works across different + environments (development, staging, production) + + Raises: + ValueError: If the endpoint is a full URL instead of a relative path """ super().__init__() + + # Validate that endpoint is a relative path and not a full URL + if "://" in endpoint or endpoint.startswith("//"): + raise ValueError( + "Endpoint must be a relative path (e.g., '/messages/'), not a full URL." + ) + + # Ensure endpoint starts with a forward slash + if not endpoint.startswith("/"): + endpoint = "/" + endpoint + self._endpoint = endpoint self._read_stream_writers = {} logger.debug(f"SseServerTransport initialized with endpoint: {endpoint}") From 1f677a9aaf5b0eabcb27830410b4418a7d858bef Mon Sep 17 00:00:00 2001 From: Aishwarya Date: Sat, 7 Jun 2025 14:56:37 +0545 Subject: [PATCH 2/5] Fix Flaky test. Removed usage of snapshot in parametrised test as they don't work well together and end up failing the test. --- tests/client/test_auth.py | 5 +---- tests/issues/test_188_concurrency.py | 23 ++++++++++++++++------- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/tests/client/test_auth.py b/tests/client/test_auth.py index 2edaff946..d4fc9ce63 100644 --- a/tests/client/test_auth.py +++ b/tests/client/test_auth.py @@ -10,7 +10,6 @@ import httpx import pytest -from inline_snapshot import snapshot from pydantic import AnyHttpUrl from mcp.client.auth import OAuthClientProvider @@ -968,8 +967,7 @@ def test_build_metadata( revocation_options=RevocationOptions(enabled=True), ) - assert metadata == snapshot( - OAuthMetadata( + assert metadata == OAuthMetadata( issuer=AnyHttpUrl(issuer_url), authorization_endpoint=AnyHttpUrl(authorization_endpoint), token_endpoint=AnyHttpUrl(token_endpoint), @@ -982,4 +980,3 @@ def test_build_metadata( revocation_endpoint_auth_methods_supported=["client_secret_post"], code_challenge_methods_supported=["S256"], ) - ) diff --git a/tests/issues/test_188_concurrency.py b/tests/issues/test_188_concurrency.py index d0a86885f..dce2b35d7 100644 --- a/tests/issues/test_188_concurrency.py +++ b/tests/issues/test_188_concurrency.py @@ -14,29 +14,38 @@ @pytest.mark.anyio async def test_messages_are_executed_concurrently(): server = FastMCP("test") - + call_timestamps = [] + @server.tool("sleep") async def sleep_tool(): + call_timestamps.append(("tool_start_time", anyio.current_time())) await anyio.sleep(_sleep_time_seconds) + call_timestamps.append(("tool_end_time", anyio.current_time())) return "done" @server.resource(_resource_name) async def slow_resource(): + call_timestamps.append(("resource_start_time", anyio.current_time())) await anyio.sleep(_sleep_time_seconds) + call_timestamps.append(("resource_end_time", anyio.current_time())) return "slow" async with create_session(server._mcp_server) as client_session: - start_time = anyio.current_time() async with anyio.create_task_group() as tg: for _ in range(10): tg.start_soon(client_session.call_tool, "sleep") tg.start_soon(client_session.read_resource, AnyUrl(_resource_name)) - end_time = anyio.current_time() - - duration = end_time - start_time - assert duration < 6 * _sleep_time_seconds - print(duration) + active_calls = 0 + max_concurrent_calls = 0 + for call_type, _ in sorted(call_timestamps, key=lambda x: x[1]): + if "start" in call_type: + active_calls += 1 + max_concurrent_calls = max(max_concurrent_calls, active_calls) + else: + active_calls -= 1 + print(f"Max concurrent calls: {max_concurrent_calls}") + assert max_concurrent_calls > 1, "No concurrent calls were executed" def main(): From 00ce787c44c88ffc3451893513ecaa1ca6463b0b Mon Sep 17 00:00:00 2001 From: Aishwarya Date: Sat, 7 Jun 2025 15:14:01 +0545 Subject: [PATCH 3/5] Lint fix. --- src/mcp/server/sse.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/mcp/server/sse.py b/src/mcp/server/sse.py index 62c5e293d..dbc4e38d4 100644 --- a/src/mcp/server/sse.py +++ b/src/mcp/server/sse.py @@ -78,7 +78,8 @@ def __init__(self, endpoint: str) -> None: messages to the relative path given. Args: - endpoint: A relative path where messages should be posted (e.g., "/messages/") + endpoint: A relative path where messages should be posted + (e.g., "/messages/"). Note: We use relative paths instead of full URLs for several reasons: @@ -94,17 +95,17 @@ def __init__(self, endpoint: str) -> None: """ super().__init__() - + # Validate that endpoint is a relative path and not a full URL if "://" in endpoint or endpoint.startswith("//"): raise ValueError( "Endpoint must be a relative path (e.g., '/messages/'), not a full URL." ) - + # Ensure endpoint starts with a forward slash if not endpoint.startswith("/"): endpoint = "/" + endpoint - + self._endpoint = endpoint self._read_stream_writers = {} logger.debug(f"SseServerTransport initialized with endpoint: {endpoint}") From 9924abefbfcf39915b21d30700a00139dc0bcbec Mon Sep 17 00:00:00 2001 From: Aishwarya Date: Wed, 11 Jun 2025 15:57:30 +0545 Subject: [PATCH 4/5] keep snapshot --- tests/client/test_auth.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/client/test_auth.py b/tests/client/test_auth.py index d4fc9ce63..2edaff946 100644 --- a/tests/client/test_auth.py +++ b/tests/client/test_auth.py @@ -10,6 +10,7 @@ import httpx import pytest +from inline_snapshot import snapshot from pydantic import AnyHttpUrl from mcp.client.auth import OAuthClientProvider @@ -967,7 +968,8 @@ def test_build_metadata( revocation_options=RevocationOptions(enabled=True), ) - assert metadata == OAuthMetadata( + assert metadata == snapshot( + OAuthMetadata( issuer=AnyHttpUrl(issuer_url), authorization_endpoint=AnyHttpUrl(authorization_endpoint), token_endpoint=AnyHttpUrl(token_endpoint), @@ -980,3 +982,4 @@ def test_build_metadata( revocation_endpoint_auth_methods_supported=["client_secret_post"], code_challenge_methods_supported=["S256"], ) + ) From 0126d1be9df5c5f8d0e08193bd7588d3ddd5f389 Mon Sep 17 00:00:00 2001 From: Aishwarya Date: Sat, 28 Jun 2025 18:51:13 +0530 Subject: [PATCH 5/5] move unrelated changes to different CL --- tests/issues/test_188_concurrency.py | 23 +++++++---------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/tests/issues/test_188_concurrency.py b/tests/issues/test_188_concurrency.py index dce2b35d7..d0a86885f 100644 --- a/tests/issues/test_188_concurrency.py +++ b/tests/issues/test_188_concurrency.py @@ -14,38 +14,29 @@ @pytest.mark.anyio async def test_messages_are_executed_concurrently(): server = FastMCP("test") - call_timestamps = [] - + @server.tool("sleep") async def sleep_tool(): - call_timestamps.append(("tool_start_time", anyio.current_time())) await anyio.sleep(_sleep_time_seconds) - call_timestamps.append(("tool_end_time", anyio.current_time())) return "done" @server.resource(_resource_name) async def slow_resource(): - call_timestamps.append(("resource_start_time", anyio.current_time())) await anyio.sleep(_sleep_time_seconds) - call_timestamps.append(("resource_end_time", anyio.current_time())) return "slow" async with create_session(server._mcp_server) as client_session: + start_time = anyio.current_time() async with anyio.create_task_group() as tg: for _ in range(10): tg.start_soon(client_session.call_tool, "sleep") tg.start_soon(client_session.read_resource, AnyUrl(_resource_name)) - active_calls = 0 - max_concurrent_calls = 0 - for call_type, _ in sorted(call_timestamps, key=lambda x: x[1]): - if "start" in call_type: - active_calls += 1 - max_concurrent_calls = max(max_concurrent_calls, active_calls) - else: - active_calls -= 1 - print(f"Max concurrent calls: {max_concurrent_calls}") - assert max_concurrent_calls > 1, "No concurrent calls were executed" + end_time = anyio.current_time() + + duration = end_time - start_time + assert duration < 6 * _sleep_time_seconds + print(duration) def main():