From c3a6667f9b7b093f909ae1f63128ab08be3e97d7 Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Mon, 12 May 2025 16:34:19 -0400 Subject: [PATCH 1/2] Remove patch and use upstream SSEServerTransport --- pyproject.toml | 2 +- src/fastmcp/low_level/sse_server_transport.py | 104 ------------------ src/fastmcp/server/http.py | 2 +- 3 files changed, 2 insertions(+), 106 deletions(-) delete mode 100644 src/fastmcp/low_level/sse_server_transport.py diff --git a/pyproject.toml b/pyproject.toml index ce86e2df7..3181fbf1a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ dependencies = [ "python-dotenv>=1.1.0", "exceptiongroup>=1.2.2", "httpx>=0.28.1", - "mcp>=1.8.0,<2.0.0", + "mcp>=1.8.1,<2.0.0", "openapi-pydantic>=0.5.1", "rich>=13.9.4", "typer>=0.15.2", diff --git a/src/fastmcp/low_level/sse_server_transport.py b/src/fastmcp/low_level/sse_server_transport.py deleted file mode 100644 index 21df959e7..000000000 --- a/src/fastmcp/low_level/sse_server_transport.py +++ /dev/null @@ -1,104 +0,0 @@ -import logging -from contextlib import asynccontextmanager -from typing import Any -from urllib.parse import quote -from uuid import uuid4 - -import anyio -from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream -from mcp.server.sse import SseServerTransport as LowLevelSSEServerTransport -from mcp.shared.message import SessionMessage -from sse_starlette import EventSourceResponse -from starlette.types import Receive, Scope, Send - -logger = logging.getLogger(__name__) - - -class SseServerTransport(LowLevelSSEServerTransport): - """ - Patched SSE server transport - """ - - @asynccontextmanager - async def connect_sse(self, scope: Scope, receive: Receive, send: Send): - """ - See https://github.com/modelcontextprotocol/python-sdk/pull/659/ - """ - if scope["type"] != "http": - logger.error("connect_sse received non-HTTP request") - raise ValueError("connect_sse can only handle HTTP requests") - - logger.debug("Setting up SSE connection") - read_stream: MemoryObjectReceiveStream[SessionMessage | Exception] - read_stream_writer: MemoryObjectSendStream[SessionMessage | Exception] - - write_stream: MemoryObjectSendStream[SessionMessage] - write_stream_reader: MemoryObjectReceiveStream[SessionMessage] - - read_stream_writer, read_stream = anyio.create_memory_object_stream(0) - write_stream, write_stream_reader = anyio.create_memory_object_stream(0) - - session_id = uuid4() - self._read_stream_writers[session_id] = read_stream_writer - logger.debug(f"Created new session with ID: {session_id}") - - # Determine the full path for the message endpoint to be sent to the client. - # scope['root_path'] is the prefix where the current Starlette app - # instance is mounted. - # e.g., "" if top-level, or "/api_prefix" if mounted under "/api_prefix". - root_path = scope.get("root_path", "") - - # self._endpoint is the path *within* this app, e.g., "/messages". - # Concatenating them gives the full absolute path from the server root. - # e.g., "" + "/messages" -> "/messages" - # e.g., "/api_prefix" + "/messages" -> "/api_prefix/messages" - full_message_path_for_client = root_path.rstrip("/") + self._endpoint - - # This is the URI (path + query) the client will use to POST messages. - client_post_uri_data = ( - f"{quote(full_message_path_for_client)}?session_id={session_id.hex}" - ) - - sse_stream_writer, sse_stream_reader = anyio.create_memory_object_stream[ - dict[str, Any] - ](0) - - async def sse_writer(): - logger.debug("Starting SSE writer") - async with sse_stream_writer, write_stream_reader: - await sse_stream_writer.send( - {"event": "endpoint", "data": client_post_uri_data} - ) - logger.debug(f"Sent endpoint event: {client_post_uri_data}") - - async for session_message in write_stream_reader: - logger.debug(f"Sending message via SSE: {session_message}") - await sse_stream_writer.send( - { - "event": "message", - "data": session_message.message.model_dump_json( - by_alias=True, exclude_none=True - ), - } - ) - - async with anyio.create_task_group() as tg: - - async def response_wrapper(scope: Scope, receive: Receive, send: Send): - """ - The EventSourceResponse returning signals a client close / disconnect. - In this case we close our side of the streams to signal the client that - the connection has been closed. - """ - await EventSourceResponse( - content=sse_stream_reader, data_sender_callable=sse_writer - )(scope, receive, send) - await read_stream_writer.aclose() - await write_stream_reader.aclose() - logging.debug(f"Client session disconnected {session_id}") - - logger.debug("Starting SSE response task") - tg.start_soon(response_wrapper, scope, receive, send) - - logger.debug("Yielding read and write streams") - yield (read_stream, write_stream) diff --git a/src/fastmcp/server/http.py b/src/fastmcp/server/http.py index 5257c24b7..f4ed22b2b 100644 --- a/src/fastmcp/server/http.py +++ b/src/fastmcp/server/http.py @@ -13,6 +13,7 @@ from mcp.server.auth.middleware.bearer_auth import ( from mcp.server.auth.provider import OAuthAuthorizationServerProvider from mcp.server.auth.routes import create_auth_routes from mcp.server.auth.settings import AuthSettings +from mcp.server.sse import SseServerTransport from mcp.server.streamable_http_manager import StreamableHTTPSessionManager from starlette.applications import Starlette from starlette.middleware import Middleware @@ -22,7 +23,6 @@ from starlette.responses import Response from starlette.routing import BaseRoute, Mount, Route from starlette.types import Receive, Scope, Send -from fastmcp.low_level.sse_server_transport import SseServerTransport from fastmcp.utilities.logging import get_logger if TYPE_CHECKING: From bfcb1277bb6f958288d49c1c395f92adc464284e Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Mon, 12 May 2025 16:35:19 -0400 Subject: [PATCH 2/2] Update pyproject.toml --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 3181fbf1a..ddcab19d4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ dependencies = [ "python-dotenv>=1.1.0", "exceptiongroup>=1.2.2", "httpx>=0.28.1", - "mcp>=1.8.1,<2.0.0", + "mcp>=1.8.2,<2.0.0", "openapi-pydantic>=0.5.1", "rich>=13.9.4", "typer>=0.15.2",