From 8d1c0c5db01800ae8782ebb0b1c980869d3a002a Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Wed, 12 Mar 2025 12:35:22 +0100 Subject: [PATCH 01/12] Close resources --- src/mcp/shared/session.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/mcp/shared/session.py b/src/mcp/shared/session.py index d0dcaee84..8ecef0929 100644 --- a/src/mcp/shared/session.py +++ b/src/mcp/shared/session.py @@ -250,11 +250,14 @@ async def send_request( ), ) ) - - if isinstance(response_or_error, JSONRPCError): - raise McpError(response_or_error.error) else: - return result_type.model_validate(response_or_error.result) + if isinstance(response_or_error, JSONRPCError): + raise McpError(response_or_error.error) + else: + return result_type.model_validate(response_or_error.result) + finally: + await response_stream.aclose() + await response_stream_reader.aclose() async def send_notification(self, notification: SendNotificationT) -> None: """ From 8090c5833465f7b4e8341090521efae81b01031d Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Thu, 13 Mar 2025 08:48:51 +0100 Subject: [PATCH 02/12] Close all resources --- pyproject.toml | 10 ++++++++++ src/mcp/client/session.py | 10 +++++++--- src/mcp/server/fastmcp/tools/base.py | 6 +++--- .../server/fastmcp/utilities/func_metadata.py | 4 +++- src/mcp/shared/session.py | 15 ++++++++++++--- tests/client/test_session.py | 4 ++++ tests/issues/test_192_request_id.py | 8 +++++++- tests/server/test_lifespan.py | 18 +++++++++++++++--- 8 files changed, 61 insertions(+), 14 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 956d9c8ce..6ec9970cf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -67,6 +67,9 @@ packages = ["src/mcp"] include = ["src/mcp", "tests"] venvPath = "." venv = ".venv" +strict = [ + "src/mcp/server/fastmcp/tools/base.py", +] [tool.ruff.lint] select = ["E", "F", "I"] @@ -85,3 +88,10 @@ members = ["examples/servers/*"] [tool.uv.sources] mcp = { workspace = true } + +# TODO(Marcelo): This should be enabled!!! There are a lot of resource warnings. +[tool.pytest.ini_options] +xfail_strict = true +filterwarnings = [ + "error", +] diff --git a/src/mcp/client/session.py b/src/mcp/client/session.py index c1cc5b5fc..cde3103b6 100644 --- a/src/mcp/client/session.py +++ b/src/mcp/client/session.py @@ -43,7 +43,9 @@ async def _default_list_roots_callback( ) -ClientResponse = TypeAdapter(types.ClientResult | types.ErrorData) +ClientResponse: TypeAdapter[types.ClientResult | types.ErrorData] = TypeAdapter( + types.ClientResult | types.ErrorData +) class ClientSession( @@ -219,7 +221,7 @@ async def unsubscribe_resource(self, uri: AnyUrl) -> types.EmptyResult: ) async def call_tool( - self, name: str, arguments: dict | None = None + self, name: str, arguments: dict[str, Any] | None = None ) -> types.CallToolResult: """Send a tools/call request.""" return await self.send_request( @@ -258,7 +260,9 @@ async def get_prompt( ) async def complete( - self, ref: types.ResourceReference | types.PromptReference, argument: dict + self, + ref: types.ResourceReference | types.PromptReference, + argument: dict[str, str], ) -> types.CompleteResult: """Send a completion/complete request.""" return await self.send_request( diff --git a/src/mcp/server/fastmcp/tools/base.py b/src/mcp/server/fastmcp/tools/base.py index da5d9348c..bf68dc02b 100644 --- a/src/mcp/server/fastmcp/tools/base.py +++ b/src/mcp/server/fastmcp/tools/base.py @@ -18,10 +18,10 @@ class Tool(BaseModel): """Internal tool registration info.""" - fn: Callable = Field(exclude=True) + fn: Callable[..., Any] = Field(exclude=True) name: str = Field(description="Name of the tool") description: str = Field(description="Description of what the tool does") - parameters: dict = Field(description="JSON schema for tool parameters") + parameters: dict[str, Any] = Field(description="JSON schema for tool parameters") fn_metadata: FuncMetadata = Field( description="Metadata about the function including a pydantic model for tool" " arguments" @@ -34,7 +34,7 @@ class Tool(BaseModel): @classmethod def from_function( cls, - fn: Callable, + fn: Callable[..., Any], name: str | None = None, description: str | None = None, context_kwarg: str | None = None, diff --git a/src/mcp/server/fastmcp/utilities/func_metadata.py b/src/mcp/server/fastmcp/utilities/func_metadata.py index cf93049e3..7bcc9bafb 100644 --- a/src/mcp/server/fastmcp/utilities/func_metadata.py +++ b/src/mcp/server/fastmcp/utilities/func_metadata.py @@ -102,7 +102,9 @@ def pre_parse_json(self, data: dict[str, Any]) -> dict[str, Any]: ) -def func_metadata(func: Callable, skip_names: Sequence[str] = ()) -> FuncMetadata: +def func_metadata( + func: Callable[..., Any], skip_names: Sequence[str] = () +) -> FuncMetadata: """Given a function, return metadata including a pydantic model representing its signature. diff --git a/src/mcp/shared/session.py b/src/mcp/shared/session.py index 8ecef0929..45e2f7894 100644 --- a/src/mcp/shared/session.py +++ b/src/mcp/shared/session.py @@ -1,4 +1,5 @@ import logging +from contextlib import AsyncExitStack from datetime import timedelta from typing import Any, Callable, Generic, TypeVar @@ -180,6 +181,7 @@ def __init__( self._read_timeout_seconds = read_timeout_seconds self._in_flight = {} + self._exit_stack = AsyncExitStack() self._incoming_message_stream_writer, self._incoming_message_stream_reader = ( anyio.create_memory_object_stream[ RequestResponder[ReceiveRequestT, SendResultT] @@ -187,6 +189,12 @@ def __init__( | Exception ]() ) + self._exit_stack.push_async_callback( + lambda: self._incoming_message_stream_reader.aclose() + ) + self._exit_stack.push_async_callback( + lambda: self._incoming_message_stream_writer.aclose() + ) async def __aenter__(self) -> Self: self._task_group = anyio.create_task_group() @@ -195,6 +203,7 @@ async def __aenter__(self) -> Self: return self async def __aexit__(self, exc_type, exc_val, exc_tb): + await self._exit_stack.aclose() # Using BaseSession as a context manager should not block on exit (this # would be very surprising behavior), so make sure to cancel the tasks # in the task group. @@ -222,6 +231,9 @@ async def send_request( ](1) self._response_streams[request_id] = response_stream + self._exit_stack.push_async_callback(lambda: response_stream.aclose()) + self._exit_stack.push_async_callback(lambda: response_stream_reader.aclose()) + jsonrpc_request = JSONRPCRequest( jsonrpc="2.0", id=request_id, @@ -255,9 +267,6 @@ async def send_request( raise McpError(response_or_error.error) else: return result_type.model_validate(response_or_error.result) - finally: - await response_stream.aclose() - await response_stream_reader.aclose() async def send_notification(self, notification: SendNotificationT) -> None: """ diff --git a/tests/client/test_session.py b/tests/client/test_session.py index 90de898c9..7d579cdac 100644 --- a/tests/client/test_session.py +++ b/tests/client/test_session.py @@ -83,6 +83,10 @@ async def listen_session(): async with ( ClientSession(server_to_client_receive, client_to_server_send) as session, anyio.create_task_group() as tg, + client_to_server_send, + client_to_server_receive, + server_to_client_send, + server_to_client_receive, ): tg.start_soon(mock_server) tg.start_soon(listen_session) diff --git a/tests/issues/test_192_request_id.py b/tests/issues/test_192_request_id.py index 628f00f99..00e187895 100644 --- a/tests/issues/test_192_request_id.py +++ b/tests/issues/test_192_request_id.py @@ -43,7 +43,13 @@ async def run_server(): ) # Start server task - async with anyio.create_task_group() as tg: + async with ( + anyio.create_task_group() as tg, + client_writer, + client_reader, + server_writer, + server_reader, + ): tg.start_soon(run_server) # Send initialize request diff --git a/tests/server/test_lifespan.py b/tests/server/test_lifespan.py index 14afb6b06..37a52969a 100644 --- a/tests/server/test_lifespan.py +++ b/tests/server/test_lifespan.py @@ -25,7 +25,7 @@ async def test_lowlevel_server_lifespan(): """Test that lifespan works in low-level server.""" @asynccontextmanager - async def test_lifespan(server: Server) -> AsyncIterator[dict]: + async def test_lifespan(server: Server) -> AsyncIterator[dict[str, bool]]: """Test lifespan context that tracks startup/shutdown.""" context = {"started": False, "shutdown": False} try: @@ -50,7 +50,13 @@ async def check_lifespan(name: str, arguments: dict) -> list: return [{"type": "text", "text": "true"}] # Run server in background task - async with anyio.create_task_group() as tg: + async with ( + anyio.create_task_group() as tg, + send_stream1, + receive_stream1, + send_stream2, + receive_stream2, + ): async def run_server(): await server.run( @@ -147,7 +153,13 @@ def check_lifespan(ctx: Context) -> bool: return True # Run server in background task - async with anyio.create_task_group() as tg: + async with ( + anyio.create_task_group() as tg, + send_stream1, + receive_stream1, + send_stream2, + receive_stream2, + ): async def run_server(): await server._mcp_server.run( From 3aab9e0565af9ab6bf46ac7002c8adc26d532c21 Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Thu, 13 Mar 2025 08:49:38 +0100 Subject: [PATCH 03/12] Update pyproject.toml --- pyproject.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 6ec9970cf..5cc49f584 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -89,7 +89,6 @@ members = ["examples/servers/*"] [tool.uv.sources] mcp = { workspace = true } -# TODO(Marcelo): This should be enabled!!! There are a lot of resource warnings. [tool.pytest.ini_options] xfail_strict = true filterwarnings = [ From 7ecc70957b8416c8f87138b184edf14af0647398 Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Thu, 13 Mar 2025 08:51:02 +0100 Subject: [PATCH 04/12] Close all resources --- src/mcp/shared/session.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/mcp/shared/session.py b/src/mcp/shared/session.py index 45e2f7894..31f888246 100644 --- a/src/mcp/shared/session.py +++ b/src/mcp/shared/session.py @@ -262,11 +262,11 @@ async def send_request( ), ) ) + + if isinstance(response_or_error, JSONRPCError): + raise McpError(response_or_error.error) else: - if isinstance(response_or_error, JSONRPCError): - raise McpError(response_or_error.error) - else: - return result_type.model_validate(response_or_error.result) + return result_type.model_validate(response_or_error.result) async def send_notification(self, notification: SendNotificationT) -> None: """ From e69309203fafccde00f33bbc9daf83c90bec958c Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Thu, 13 Mar 2025 08:53:02 +0100 Subject: [PATCH 05/12] Close all resources --- pyproject.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 5cc49f584..bf8b8a99e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -93,4 +93,6 @@ mcp = { workspace = true } xfail_strict = true filterwarnings = [ "error", + # This should be fixed on Uvicorn's side. + "ignore::DeprecationWarning:websockets", ] From 600a3f0a6631490f5ad0cb44a51057aada47f1d6 Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Thu, 13 Mar 2025 08:55:15 +0100 Subject: [PATCH 06/12] try now... --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index bf8b8a99e..b48a5b69d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -95,4 +95,5 @@ filterwarnings = [ "error", # This should be fixed on Uvicorn's side. "ignore::DeprecationWarning:websockets", + "ignore::DeprecationWarning:websockets.server", ] From d072c2880e5f71c30493a43e6252df0266f4af43 Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Thu, 13 Mar 2025 08:55:30 +0100 Subject: [PATCH 07/12] try to ignore this --- pyproject.toml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index b48a5b69d..1195ece06 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -94,6 +94,5 @@ xfail_strict = true filterwarnings = [ "error", # This should be fixed on Uvicorn's side. - "ignore::DeprecationWarning:websockets", - "ignore::DeprecationWarning:websockets.server", + "ignore::DeprecationWarning:websockets.*", ] From 356bf35f86e8cc898d1d4585b5197d5eb43d58f8 Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Thu, 13 Mar 2025 08:57:04 +0100 Subject: [PATCH 08/12] try again --- pyproject.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 1195ece06..b20c8be68 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -94,5 +94,6 @@ xfail_strict = true filterwarnings = [ "error", # This should be fixed on Uvicorn's side. - "ignore::DeprecationWarning:websockets.*", + "ignore::DeprecationWarning:websockets", + "ignore:websockets.server.WebSocketServerProtocol is deprecated:DeprecationWarning", ] From 91fbb9da1b4dd95eb1989ec27308440c237eed08 Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Thu, 13 Mar 2025 08:59:00 +0100 Subject: [PATCH 09/12] try adding one more.. --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index b20c8be68..1c7406cc8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -96,4 +96,5 @@ filterwarnings = [ # This should be fixed on Uvicorn's side. "ignore::DeprecationWarning:websockets", "ignore:websockets.server.WebSocketServerProtocol is deprecated:DeprecationWarning", + "ignore:Returning str or bytes from read_resource is deprecated. Use Iterable[ReadResourceContents] instead.:mcp.shared.exceptions.McpError" ] From e3d6af2a844fedfca443c3361a5bb97b1dbbbf3a Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Thu, 13 Mar 2025 09:01:51 +0100 Subject: [PATCH 10/12] try now --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 1c7406cc8..7a6bdd877 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -96,5 +96,5 @@ filterwarnings = [ # This should be fixed on Uvicorn's side. "ignore::DeprecationWarning:websockets", "ignore:websockets.server.WebSocketServerProtocol is deprecated:DeprecationWarning", - "ignore:Returning str or bytes from read_resource is deprecated. Use Iterable[ReadResourceContents] instead.:mcp.shared.exceptions.McpError" + "ignore:Returning str or bytes from read_resource is deprecated. Use Iterable[ReadResourceContents] instead.:DeprecationWarning" ] From 03ffbf7bbb757c0a5cc66852b9d94336917d9fc8 Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Thu, 13 Mar 2025 09:10:39 +0100 Subject: [PATCH 11/12] try now --- .github/workflows/shared.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/shared.yml b/.github/workflows/shared.yml index 2be9f129c..2f511c803 100644 --- a/.github/workflows/shared.yml +++ b/.github/workflows/shared.yml @@ -62,8 +62,5 @@ jobs: with: python-version-file: ".python-version" - - name: Install the project - run: uv sync --frozen --all-extras --dev - - name: Run pytest - run: uv run --frozen pytest + run: uv run --frozen --all-extras --dev pytest From 527f9c700b61c1675f359bf7302415c0a98cddf3 Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Thu, 13 Mar 2025 09:13:18 +0100 Subject: [PATCH 12/12] revert ci changes --- .github/workflows/shared.yml | 5 ++++- pyproject.toml | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/shared.yml b/.github/workflows/shared.yml index 2f511c803..2be9f129c 100644 --- a/.github/workflows/shared.yml +++ b/.github/workflows/shared.yml @@ -62,5 +62,8 @@ jobs: with: python-version-file: ".python-version" + - name: Install the project + run: uv sync --frozen --all-extras --dev + - name: Run pytest - run: uv run --frozen --all-extras --dev pytest + run: uv run --frozen pytest diff --git a/pyproject.toml b/pyproject.toml index 7a6bdd877..157263de6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -96,5 +96,5 @@ filterwarnings = [ # This should be fixed on Uvicorn's side. "ignore::DeprecationWarning:websockets", "ignore:websockets.server.WebSocketServerProtocol is deprecated:DeprecationWarning", - "ignore:Returning str or bytes from read_resource is deprecated. Use Iterable[ReadResourceContents] instead.:DeprecationWarning" + "ignore:Returning str or bytes.*:DeprecationWarning:mcp.server.lowlevel" ]