From 3ea27386873f8a8177dabeb650e36c5ee13814e0 Mon Sep 17 00:00:00 2001 From: Basil Hosmer Date: Thu, 10 Apr 2025 13:51:44 -0400 Subject: [PATCH 1/6] Add support and tests for ToolAnnotations in FastMCP and lowlevel servers --- src/mcp/server/fastmcp/server.py | 21 ++- src/mcp/server/fastmcp/tools/base.py | 5 + src/mcp/server/fastmcp/tools/tool_manager.py | 5 +- src/mcp/types.py | 50 +++++++ tests/server/fastmcp/test_tool_manager.py | 40 ++++++ .../server/test_lowlevel_tool_annotations.py | 125 ++++++++++++++++++ 6 files changed, 242 insertions(+), 4 deletions(-) create mode 100644 tests/server/test_lowlevel_tool_annotations.py diff --git a/src/mcp/server/fastmcp/server.py b/src/mcp/server/fastmcp/server.py index bf0ce880a..263e1c9ce 100644 --- a/src/mcp/server/fastmcp/server.py +++ b/src/mcp/server/fastmcp/server.py @@ -172,12 +172,17 @@ def _setup_handlers(self) -> None: async def list_tools(self) -> list[MCPTool]: """List all available tools.""" + from mcp.types import ToolAnnotations + tools = self._tool_manager.list_tools() return [ MCPTool( name=info.name, description=info.description, inputSchema=info.parameters, + annotations=ToolAnnotations.model_validate(info.annotations) + if info.annotations + else None, ) for info in tools ] @@ -246,6 +251,7 @@ def add_tool( fn: AnyFunction, name: str | None = None, description: str | None = None, + annotations: dict[str, Any] | None = None, ) -> None: """Add a tool to the server. @@ -256,11 +262,17 @@ def add_tool( fn: The function to register as a tool name: Optional name for the tool (defaults to function name) description: Optional description of what the tool does + annotations: Optional annotations providing additional tool information """ - self._tool_manager.add_tool(fn, name=name, description=description) + self._tool_manager.add_tool( + fn, name=name, description=description, annotations=annotations + ) def tool( - self, name: str | None = None, description: str | None = None + self, + name: str | None = None, + description: str | None = None, + annotations: dict[str, Any] | None = None, ) -> Callable[[AnyFunction], AnyFunction]: """Decorator to register a tool. @@ -271,6 +283,7 @@ def tool( Args: name: Optional name for the tool (defaults to function name) description: Optional description of what the tool does + annotations: Optional annotations providing additional tool information Example: @server.tool() @@ -295,7 +308,9 @@ async def async_tool(x: int, context: Context) -> str: ) def decorator(fn: AnyFunction) -> AnyFunction: - self.add_tool(fn, name=name, description=description) + self.add_tool( + fn, name=name, description=description, annotations=annotations + ) return fn return decorator diff --git a/src/mcp/server/fastmcp/tools/base.py b/src/mcp/server/fastmcp/tools/base.py index 92a216f56..f0c6b8381 100644 --- a/src/mcp/server/fastmcp/tools/base.py +++ b/src/mcp/server/fastmcp/tools/base.py @@ -30,6 +30,9 @@ class Tool(BaseModel): context_kwarg: str | None = Field( None, description="Name of the kwarg that should receive context" ) + annotations: dict[str, Any] | None = Field( + None, description="Optional annotations for the tool" + ) @classmethod def from_function( @@ -38,6 +41,7 @@ def from_function( name: str | None = None, description: str | None = None, context_kwarg: str | None = None, + annotations: dict[str, Any] | None = None, ) -> Tool: """Create a Tool from a function.""" from mcp.server.fastmcp import Context @@ -73,6 +77,7 @@ def from_function( fn_metadata=func_arg_metadata, is_async=is_async, context_kwarg=context_kwarg, + annotations=annotations, ) async def run( diff --git a/src/mcp/server/fastmcp/tools/tool_manager.py b/src/mcp/server/fastmcp/tools/tool_manager.py index 4d6ac268f..8ef67b995 100644 --- a/src/mcp/server/fastmcp/tools/tool_manager.py +++ b/src/mcp/server/fastmcp/tools/tool_manager.py @@ -35,9 +35,12 @@ def add_tool( fn: Callable[..., Any], name: str | None = None, description: str | None = None, + annotations: dict[str, Any] | None = None, ) -> Tool: """Add a tool to the server.""" - tool = Tool.from_function(fn, name=name, description=description) + tool = Tool.from_function( + fn, name=name, description=description, annotations=annotations + ) existing = self._tools.get(tool.name) if existing: if self.warn_on_duplicate_tools: diff --git a/src/mcp/types.py b/src/mcp/types.py index bd71d51f0..6ab7fba5c 100644 --- a/src/mcp/types.py +++ b/src/mcp/types.py @@ -705,6 +705,54 @@ class ListToolsRequest(PaginatedRequest[RequestParams | None, Literal["tools/lis params: RequestParams | None = None +class ToolAnnotations(BaseModel): + """ + Additional properties describing a Tool to clients. + + NOTE: all properties in ToolAnnotations are **hints**. + They are not guaranteed to provide a faithful description of + tool behavior (including descriptive properties like `title`). + + Clients should never make tool use decisions based on ToolAnnotations + received from untrusted servers. + """ + + title: str | None = None + """A human-readable title for the tool.""" + + readOnlyHint: bool | None = None + """ + If true, the tool does not modify its environment. + Default: false + """ + + destructiveHint: bool | None = None + """ + If true, the tool may perform destructive updates to its environment. + If false, the tool performs only additive updates. + (This property is meaningful only when `readOnlyHint == false`) + Default: true + """ + + idempotentHint: bool | None = None + """ + If true, calling the tool repeatedly with the same arguments + will have no additional effect on the its environment. + (This property is meaningful only when `readOnlyHint == false`) + Default: false + """ + + openWorldHint: bool | None = None + """ + If true, this tool may interact with an "open world" of external + entities. If false, the tool's domain of interaction is closed. + For example, the world of a web search tool is open, whereas that + of a memory tool is not. + Default: true + """ + model_config = ConfigDict(extra="allow") + + class Tool(BaseModel): """Definition for a tool the client can call.""" @@ -714,6 +762,8 @@ class Tool(BaseModel): """A human-readable description of the tool.""" inputSchema: dict[str, Any] """A JSON Schema object defining the expected parameters for the tool.""" + annotations: ToolAnnotations | None = None + """Optional additional tool information.""" model_config = ConfigDict(extra="allow") diff --git a/tests/server/fastmcp/test_tool_manager.py b/tests/server/fastmcp/test_tool_manager.py index 8f52e3d85..4396b0713 100644 --- a/tests/server/fastmcp/test_tool_manager.py +++ b/tests/server/fastmcp/test_tool_manager.py @@ -321,3 +321,43 @@ def tool_with_context(x: int, ctx: Context) -> str: ctx = mcp.get_context() with pytest.raises(ToolError, match="Error executing tool tool_with_context"): await manager.call_tool("tool_with_context", {"x": 42}, context=ctx) + + +class TestToolAnnotations: + def test_tool_annotations(self): + """Test that tool annotations are correctly added to tools.""" + + def read_data(path: str) -> str: + """Read data from a file.""" + return f"Data from {path}" + + annotations = { + "title": "File Reader", + "readOnlyHint": True, + "openWorldHint": False, + } + + manager = ToolManager() + tool = manager.add_tool(read_data, annotations=annotations) + + assert tool.annotations is not None + assert tool.annotations["title"] == "File Reader" + assert tool.annotations["readOnlyHint"] is True + assert tool.annotations["openWorldHint"] is False + + @pytest.mark.anyio + async def test_tool_annotations_in_fastmcp(self): + """Test that tool annotations are included in MCPTool conversion.""" + + app = FastMCP() + + @app.tool(annotations={"title": "Echo Tool", "readOnlyHint": True}) + def echo(message: str) -> str: + """Echo a message back.""" + return message + + tools = await app.list_tools() + assert len(tools) == 1 + assert tools[0].annotations is not None + assert tools[0].annotations.title == "Echo Tool" + assert tools[0].annotations.readOnlyHint is True diff --git a/tests/server/test_lowlevel_tool_annotations.py b/tests/server/test_lowlevel_tool_annotations.py new file mode 100644 index 000000000..1bcdbe49c --- /dev/null +++ b/tests/server/test_lowlevel_tool_annotations.py @@ -0,0 +1,125 @@ +"""Tests for tool annotations in low-level server.""" + +import anyio +import pytest + +from mcp.client.session import ClientSession +from mcp.server import Server +from mcp.server.lowlevel import NotificationOptions +from mcp.server.models import InitializationOptions +from mcp.server.session import ServerSession +from mcp.shared.session import RequestResponder +from mcp.types import ( + Tool, + ToolAnnotations, + JSONRPCMessage, + ListToolsRequest, + ListToolsResult, + InitializeRequestParams, + Implementation, + ClientCapabilities, + ServerRequest, + ClientResult, + ServerNotification, +) + + +@pytest.mark.anyio +async def test_lowlevel_server_tool_annotations(): + """Test that tool annotations work in low-level server.""" + server = Server("test") + + # Create a tool with annotations + @server.list_tools() + async def list_tools(): + return [ + Tool( + name="echo", + description="Echo a message back", + inputSchema={ + "type": "object", + "properties": { + "message": {"type": "string"}, + }, + "required": ["message"], + }, + annotations=ToolAnnotations( + title="Echo Tool", + readOnlyHint=True, + ), + ) + ] + + server_to_client_send, server_to_client_receive = anyio.create_memory_object_stream[ + JSONRPCMessage + ](10) + client_to_server_send, client_to_server_receive = anyio.create_memory_object_stream[ + JSONRPCMessage + ](10) + + # Track results for assertion + tools_result = None + + # Message handler for client + async def message_handler( + message: RequestResponder[ServerRequest, ClientResult] | ServerNotification | Exception, + ) -> None: + nonlocal tools_result + if isinstance(message, Exception): + raise message + if isinstance(message, RequestResponder): + result = message.message.result + if isinstance(result, dict) and "tools" in result: + tools_result = ListToolsResult.model_validate(result) + + # Server task + async def run_server(): + async with ServerSession( + client_to_server_receive, + server_to_client_send, + InitializationOptions( + server_name="test-server", + server_version="1.0.0", + capabilities=server.get_capabilities( + notification_options=NotificationOptions(), + experimental_capabilities={}, + ), + ), + ) as server_session: + async with anyio.create_task_group() as tg: + + async def handle_messages(): + async for message in server_session.incoming_messages: + await server._handle_message( + message, server_session, {}, False + ) + + tg.start_soon(handle_messages) + await anyio.sleep_forever() + + # Run the test + async with anyio.create_task_group() as tg: + tg.start_soon(run_server) + + async with ClientSession( + server_to_client_receive, + client_to_server_send, + message_handler=message_handler, + ) as client_session: + # Initialize the session + await client_session.initialize() + + # List tools + tools_result = await client_session.list_tools() + + + # Cancel the server task + tg.cancel_scope.cancel() + + # Verify results + assert tools_result is not None + assert len(tools_result.tools) == 1 + assert tools_result.tools[0].name == "echo" + assert tools_result.tools[0].annotations is not None + assert tools_result.tools[0].annotations.title == "Echo Tool" + assert tools_result.tools[0].annotations.readOnlyHint is True \ No newline at end of file From 452278e3022be23ca50511787f7d6c5dea7438ad Mon Sep 17 00:00:00 2001 From: Basil Hosmer Date: Thu, 10 Apr 2025 13:57:48 -0400 Subject: [PATCH 2/6] Fix type errors in lowlevel tool annotations test --- .../server/test_lowlevel_tool_annotations.py | 29 +++++++------------ 1 file changed, 10 insertions(+), 19 deletions(-) diff --git a/tests/server/test_lowlevel_tool_annotations.py b/tests/server/test_lowlevel_tool_annotations.py index 1bcdbe49c..1cd7a5514 100644 --- a/tests/server/test_lowlevel_tool_annotations.py +++ b/tests/server/test_lowlevel_tool_annotations.py @@ -44,7 +44,7 @@ async def list_tools(): "required": ["message"], }, annotations=ToolAnnotations( - title="Echo Tool", + title="Echo Tool", readOnlyHint=True, ), ) @@ -57,20 +57,14 @@ async def list_tools(): JSONRPCMessage ](10) - # Track results for assertion - tools_result = None - # Message handler for client async def message_handler( - message: RequestResponder[ServerRequest, ClientResult] | ServerNotification | Exception, + message: RequestResponder[ServerRequest, ClientResult] + | ServerNotification + | Exception, ) -> None: - nonlocal tools_result if isinstance(message, Exception): raise message - if isinstance(message, RequestResponder): - result = message.message.result - if isinstance(result, dict) and "tools" in result: - tools_result = ListToolsResult.model_validate(result) # Server task async def run_server(): @@ -90,9 +84,7 @@ async def run_server(): async def handle_messages(): async for message in server_session.incoming_messages: - await server._handle_message( - message, server_session, {}, False - ) + await server._handle_message(message, server_session, {}, False) tg.start_soon(handle_messages) await anyio.sleep_forever() @@ -100,7 +92,7 @@ async def handle_messages(): # Run the test async with anyio.create_task_group() as tg: tg.start_soon(run_server) - + async with ClientSession( server_to_client_receive, client_to_server_send, @@ -108,18 +100,17 @@ async def handle_messages(): ) as client_session: # Initialize the session await client_session.initialize() - + # List tools tools_result = await client_session.list_tools() - - + # Cancel the server task tg.cancel_scope.cancel() - + # Verify results assert tools_result is not None assert len(tools_result.tools) == 1 assert tools_result.tools[0].name == "echo" assert tools_result.tools[0].annotations is not None assert tools_result.tools[0].annotations.title == "Echo Tool" - assert tools_result.tools[0].annotations.readOnlyHint is True \ No newline at end of file + assert tools_result.tools[0].annotations.readOnlyHint is True From dd10fa175c440857c4e2c97bd09b5a87f9c67b72 Mon Sep 17 00:00:00 2001 From: Basil Hosmer Date: Thu, 10 Apr 2025 14:02:13 -0400 Subject: [PATCH 3/6] Fix import sorting and remove unused imports --- tests/server/test_lowlevel_tool_annotations.py | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/tests/server/test_lowlevel_tool_annotations.py b/tests/server/test_lowlevel_tool_annotations.py index 1cd7a5514..47d03ad23 100644 --- a/tests/server/test_lowlevel_tool_annotations.py +++ b/tests/server/test_lowlevel_tool_annotations.py @@ -10,17 +10,12 @@ from mcp.server.session import ServerSession from mcp.shared.session import RequestResponder from mcp.types import ( - Tool, - ToolAnnotations, - JSONRPCMessage, - ListToolsRequest, - ListToolsResult, - InitializeRequestParams, - Implementation, - ClientCapabilities, - ServerRequest, ClientResult, + JSONRPCMessage, ServerNotification, + ServerRequest, + Tool, + ToolAnnotations, ) From fcf10517edc74f339596bed75cda2b23769271c0 Mon Sep 17 00:00:00 2001 From: Basil Hosmer Date: Tue, 29 Apr 2025 17:09:51 -0400 Subject: [PATCH 4/6] use ToolAnnotations instead of dict throughout --- pyproject.toml | 10 ++++++---- src/mcp/server/fastmcp/server.py | 15 ++++++--------- src/mcp/server/fastmcp/tools/base.py | 7 ++++--- src/mcp/server/fastmcp/tools/tool_manager.py | 3 ++- tests/server/fastmcp/test_tool_manager.py | 19 ++++++++++--------- uv.lock | 9 ++++----- 6 files changed, 32 insertions(+), 31 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 25514cd6b..095212757 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,7 +29,7 @@ dependencies = [ "starlette>=0.27", "sse-starlette>=1.6.1", "pydantic-settings>=2.5.2", - "uvicorn>=0.23.1", + "uvicorn>=0.23.1; sys_platform != 'emscripten'", ] [project.optional-dependencies] @@ -89,8 +89,8 @@ venv = ".venv" strict = ["src/mcp/**/*.py"] [tool.ruff.lint] -select = ["E", "F", "I", "UP"] -ignore = [] +select = ["C4", "E", "F", "I", "PERF", "UP"] +ignore = ["PERF203"] [tool.ruff] line-length = 88 @@ -113,5 +113,7 @@ filterwarnings = [ # This should be fixed on Uvicorn's side. "ignore::DeprecationWarning:websockets", "ignore:websockets.server.WebSocketServerProtocol is deprecated:DeprecationWarning", - "ignore:Returning str or bytes.*:DeprecationWarning:mcp.server.lowlevel" + "ignore:Returning str or bytes.*:DeprecationWarning:mcp.server.lowlevel", + # this is a problem in starlette 0.27, which we're currently pinned to + "ignore:Please use `import python_multipart` instead.:PendingDeprecationWarning", ] diff --git a/src/mcp/server/fastmcp/server.py b/src/mcp/server/fastmcp/server.py index 263e1c9ce..690b3b0fc 100644 --- a/src/mcp/server/fastmcp/server.py +++ b/src/mcp/server/fastmcp/server.py @@ -43,6 +43,7 @@ GetPromptResult, ImageContent, TextContent, + ToolAnnotations, ) from mcp.types import Prompt as MCPPrompt from mcp.types import PromptArgument as MCPPromptArgument @@ -172,17 +173,13 @@ def _setup_handlers(self) -> None: async def list_tools(self) -> list[MCPTool]: """List all available tools.""" - from mcp.types import ToolAnnotations - tools = self._tool_manager.list_tools() return [ MCPTool( name=info.name, description=info.description, inputSchema=info.parameters, - annotations=ToolAnnotations.model_validate(info.annotations) - if info.annotations - else None, + annotations=info.annotations, ) for info in tools ] @@ -251,7 +248,7 @@ def add_tool( fn: AnyFunction, name: str | None = None, description: str | None = None, - annotations: dict[str, Any] | None = None, + annotations: ToolAnnotations | None = None, ) -> None: """Add a tool to the server. @@ -262,7 +259,7 @@ def add_tool( fn: The function to register as a tool name: Optional name for the tool (defaults to function name) description: Optional description of what the tool does - annotations: Optional annotations providing additional tool information + annotations: Optional ToolAnnotations providing additional tool information """ self._tool_manager.add_tool( fn, name=name, description=description, annotations=annotations @@ -272,7 +269,7 @@ def tool( self, name: str | None = None, description: str | None = None, - annotations: dict[str, Any] | None = None, + annotations: ToolAnnotations | None = None, ) -> Callable[[AnyFunction], AnyFunction]: """Decorator to register a tool. @@ -283,7 +280,7 @@ def tool( Args: name: Optional name for the tool (defaults to function name) description: Optional description of what the tool does - annotations: Optional annotations providing additional tool information + annotations: Optional ToolAnnotations providing additional tool information Example: @server.tool() diff --git a/src/mcp/server/fastmcp/tools/base.py b/src/mcp/server/fastmcp/tools/base.py index f0c6b8381..21eb1841d 100644 --- a/src/mcp/server/fastmcp/tools/base.py +++ b/src/mcp/server/fastmcp/tools/base.py @@ -8,6 +8,7 @@ from mcp.server.fastmcp.exceptions import ToolError from mcp.server.fastmcp.utilities.func_metadata import FuncMetadata, func_metadata +from mcp.types import ToolAnnotations if TYPE_CHECKING: from mcp.server.fastmcp.server import Context @@ -30,7 +31,7 @@ class Tool(BaseModel): context_kwarg: str | None = Field( None, description="Name of the kwarg that should receive context" ) - annotations: dict[str, Any] | None = Field( + annotations: ToolAnnotations | None = Field( None, description="Optional annotations for the tool" ) @@ -41,10 +42,10 @@ def from_function( name: str | None = None, description: str | None = None, context_kwarg: str | None = None, - annotations: dict[str, Any] | None = None, + annotations: ToolAnnotations | None = None, ) -> Tool: """Create a Tool from a function.""" - from mcp.server.fastmcp import Context + from mcp.server.fastmcp.server import Context func_name = name or fn.__name__ diff --git a/src/mcp/server/fastmcp/tools/tool_manager.py b/src/mcp/server/fastmcp/tools/tool_manager.py index 8ef67b995..cfdaeb350 100644 --- a/src/mcp/server/fastmcp/tools/tool_manager.py +++ b/src/mcp/server/fastmcp/tools/tool_manager.py @@ -7,6 +7,7 @@ from mcp.server.fastmcp.tools.base import Tool from mcp.server.fastmcp.utilities.logging import get_logger from mcp.shared.context import LifespanContextT +from mcp.types import ToolAnnotations if TYPE_CHECKING: from mcp.server.fastmcp.server import Context @@ -35,7 +36,7 @@ def add_tool( fn: Callable[..., Any], name: str | None = None, description: str | None = None, - annotations: dict[str, Any] | None = None, + annotations: ToolAnnotations | None = None, ) -> Tool: """Add a tool to the server.""" tool = Tool.from_function( diff --git a/tests/server/fastmcp/test_tool_manager.py b/tests/server/fastmcp/test_tool_manager.py index 4396b0713..e36a09d54 100644 --- a/tests/server/fastmcp/test_tool_manager.py +++ b/tests/server/fastmcp/test_tool_manager.py @@ -9,6 +9,7 @@ from mcp.server.fastmcp.tools import ToolManager from mcp.server.session import ServerSessionT from mcp.shared.context import LifespanContextT +from mcp.types import ToolAnnotations class TestAddTools: @@ -331,19 +332,19 @@ def read_data(path: str) -> str: """Read data from a file.""" return f"Data from {path}" - annotations = { - "title": "File Reader", - "readOnlyHint": True, - "openWorldHint": False, - } + annotations = ToolAnnotations( + title="File Reader", + readOnlyHint=True, + openWorldHint=False, + ) manager = ToolManager() tool = manager.add_tool(read_data, annotations=annotations) assert tool.annotations is not None - assert tool.annotations["title"] == "File Reader" - assert tool.annotations["readOnlyHint"] is True - assert tool.annotations["openWorldHint"] is False + assert tool.annotations.title == "File Reader" + assert tool.annotations.readOnlyHint is True + assert tool.annotations.openWorldHint is False @pytest.mark.anyio async def test_tool_annotations_in_fastmcp(self): @@ -351,7 +352,7 @@ async def test_tool_annotations_in_fastmcp(self): app = FastMCP() - @app.tool(annotations={"title": "Echo Tool", "readOnlyHint": True}) + @app.tool(annotations=ToolAnnotations(title="Echo Tool", readOnlyHint=True)) def echo(message: str) -> str: """Echo a message back.""" return message diff --git a/uv.lock b/uv.lock index 424e2d482..ad14c0400 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,4 @@ version = 1 -revision = 1 requires-python = ">=3.10" [options] @@ -487,6 +486,7 @@ wheels = [ [[package]] name = "mcp" +version = "1.6.1.dev14+dd10fa1" source = { editable = "." } dependencies = [ { name = "anyio" }, @@ -496,7 +496,7 @@ dependencies = [ { name = "pydantic-settings" }, { name = "sse-starlette" }, { name = "starlette" }, - { name = "uvicorn" }, + { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, ] [package.optional-dependencies] @@ -540,10 +540,9 @@ requires-dist = [ { name = "sse-starlette", specifier = ">=1.6.1" }, { name = "starlette", specifier = ">=0.27" }, { name = "typer", marker = "extra == 'cli'", specifier = ">=0.12.4" }, - { name = "uvicorn", specifier = ">=0.23.1" }, + { name = "uvicorn", marker = "sys_platform != 'emscripten'", specifier = ">=0.23.1" }, { name = "websockets", marker = "extra == 'ws'", specifier = ">=15.0.1" }, ] -provides-extras = ["cli", "rich", "ws"] [package.metadata.requires-dev] dev = [ @@ -1618,4 +1617,4 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2b/fb/c492d6daa5ec067c2988ac80c61359ace5c4c674c532985ac5a123436cec/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b359ed09954d7c18bbc1680f380c7301f92c60bf924171629c5db97febb12f04", size = 174155 }, { url = "https://files.pythonhosted.org/packages/68/a1/dcb68430b1d00b698ae7a7e0194433bce4f07ded185f0ee5fb21e2a2e91e/websockets-15.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:cad21560da69f4ce7658ca2cb83138fb4cf695a2ba3e475e0559e05991aa8122", size = 176884 }, { url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743 }, -] \ No newline at end of file +] From 60c15661d3fe7d524f7e3a0f0f05884b21e09e18 Mon Sep 17 00:00:00 2001 From: Basil Hosmer Date: Tue, 29 Apr 2025 17:19:55 -0400 Subject: [PATCH 5/6] revert changes to uv.lock --- uv.lock | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/uv.lock b/uv.lock index ad14c0400..78f46f47c 100644 --- a/uv.lock +++ b/uv.lock @@ -1,4 +1,5 @@ version = 1 +revision = 1 requires-python = ">=3.10" [options] @@ -486,7 +487,6 @@ wheels = [ [[package]] name = "mcp" -version = "1.6.1.dev14+dd10fa1" source = { editable = "." } dependencies = [ { name = "anyio" }, @@ -543,6 +543,7 @@ requires-dist = [ { name = "uvicorn", marker = "sys_platform != 'emscripten'", specifier = ">=0.23.1" }, { name = "websockets", marker = "extra == 'ws'", specifier = ">=15.0.1" }, ] +provides-extras = ["cli", "rich", "ws"] [package.metadata.requires-dev] dev = [ From a1b2ed576d59c8e176917861753e6f064b9c0a0b Mon Sep 17 00:00:00 2001 From: Basil Hosmer Date: Wed, 30 Apr 2025 09:48:34 -0400 Subject: [PATCH 6/6] remove deprecation warning suppression --- pyproject.toml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 178b58c0c..dcae57bd0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -114,7 +114,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.*:DeprecationWarning:mcp.server.lowlevel", - # this is a problem in starlette 0.27, which we're currently pinned to - "ignore:Please use `import python_multipart` instead.:PendingDeprecationWarning", + "ignore:Returning str or bytes.*:DeprecationWarning:mcp.server.lowlevel" ]