From 6194d06f6ccf976ee438de3d7d92a6a961db8882 Mon Sep 17 00:00:00 2001 From: samefarrar <67292186+samefarrar@users.noreply.github.com> Date: Sun, 29 Dec 2024 14:57:34 +0000 Subject: [PATCH 01/24] test: Add test that uv command inserted into config can run "with --help" --- tests/client/test_config.py | 56 +++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 tests/client/test_config.py diff --git a/tests/client/test_config.py b/tests/client/test_config.py new file mode 100644 index 0000000..2a8273e --- /dev/null +++ b/tests/client/test_config.py @@ -0,0 +1,56 @@ +import json +import subprocess +from pathlib import Path +from unittest.mock import patch + +import pytest + +from mcp.cli.claude import update_claude_config + + +@pytest.fixture +def temp_config_dir(tmp_path): + """Create a temporary Claude config directory.""" + config_dir = tmp_path / "Claude" + config_dir.mkdir() + return config_dir + +@pytest.fixture +def mock_config_path(temp_config_dir): + """Mock get_claude_config_path to return our temporary directory.""" + with patch('mcp.cli.claude.get_claude_config_path', return_value=temp_config_dir): + yield temp_config_dir + +def test_command_execution(mock_config_path): + """Test that the generated command can actually be executed.""" + # Setup + server_name = "test_server" + file_spec = "test_server.py:app" + + # Update config + success = update_claude_config( + file_spec=file_spec, + server_name=server_name, + ) + assert success + + # Read the generated config + config_file = mock_config_path / "claude_desktop_config.json" + config = json.loads(config_file.read_text()) + + # Get the command and args + server_config = config["mcpServers"][server_name] + command = server_config["command"] + args = server_config["args"] + + test_args = [command] + args + ["--help"] + + result = subprocess.run( + test_args, + capture_output=True, + text=True, + timeout=5 + ) + + assert result.returncode == 0 + assert "usage" in result.stdout.lower() \ No newline at end of file From e65404aa05206400e9e1bc913758ceb463cbbe76 Mon Sep 17 00:00:00 2001 From: samefarrar <67292186+samefarrar@users.noreply.github.com> Date: Sun, 29 Dec 2024 15:01:25 +0000 Subject: [PATCH 02/24] fix: Install still runs the fastmcp command, which is now unecessary (/not installed). We can just run it with mcp. --- src/mcp/cli/claude.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/mcp/cli/claude.py b/src/mcp/cli/claude.py index 9e2ef6c..8522380 100644 --- a/src/mcp/cli/claude.py +++ b/src/mcp/cli/claude.py @@ -87,7 +87,7 @@ def update_claude_config( args = ["run"] # Collect all packages in a set to deduplicate - packages = {"fastmcp"} + packages = {"mcp"} if with_packages: packages.update(pkg for pkg in with_packages if pkg) @@ -107,7 +107,7 @@ def update_claude_config( file_spec = str(Path(file_spec).resolve()) # Add fastmcp run command - args.extend(["fastmcp", "run", file_spec]) + args.extend(["mcp", "run", file_spec]) server_config = { "command": "uv", From 4f36581a5cae4a46aaba3514cddc29de8867c473 Mon Sep 17 00:00:00 2001 From: TerminalMan <84923604+SecretiveShell@users.noreply.github.com> Date: Sun, 29 Dec 2024 16:41:30 +0000 Subject: [PATCH 03/24] add text encoding params to STDIO client --- src/mcp/client/stdio.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/mcp/client/stdio.py b/src/mcp/client/stdio.py index e79a816..e894ae9 100644 --- a/src/mcp/client/stdio.py +++ b/src/mcp/client/stdio.py @@ -1,6 +1,7 @@ import os import sys from contextlib import asynccontextmanager +from typing import Literal import anyio import anyio.lowlevel @@ -65,6 +66,20 @@ class StdioServerParameters(BaseModel): If not specified, the result of get_default_environment() will be used. """ + encoding: str = "utf-8" + """ + The text encoding used when sending/receiving messages to the server + + defaults to utf-8 + """ + + encoding_error_handler: Literal["strict", "ignore", "replace"] = "strict" + """ + The text encoding error handler. + + See https://docs.python.org/3/library/codecs.html#codec-base-classes for explanations of possible values + """ + @asynccontextmanager async def stdio_client(server: StdioServerParameters): @@ -93,7 +108,7 @@ async def stdio_client(server: StdioServerParameters): try: async with read_stream_writer: buffer = "" - async for chunk in TextReceiveStream(process.stdout): + async for chunk in TextReceiveStream(process.stdout, encoding=server.encoding, errors=server.encoding_error_handler): lines = (buffer + chunk).split("\n") buffer = lines.pop() @@ -115,7 +130,7 @@ async def stdio_client(server: StdioServerParameters): async with write_stream_reader: async for message in write_stream_reader: json = message.model_dump_json(by_alias=True, exclude_none=True) - await process.stdin.send((json + "\n").encode()) + await process.stdin.send((json + "\n").encode(encoding=server.encoding, errors=server.encoding_error_handler)) except anyio.ClosedResourceError: await anyio.lowlevel.checkpoint() From 99727a97ae476a26fbac8fcf202a68e525100519 Mon Sep 17 00:00:00 2001 From: TerminalMan <84923604+SecretiveShell@users.noreply.github.com> Date: Sun, 29 Dec 2024 17:08:09 +0000 Subject: [PATCH 04/24] ruff format --- src/mcp/client/stdio.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/mcp/client/stdio.py b/src/mcp/client/stdio.py index e894ae9..2eaa347 100644 --- a/src/mcp/client/stdio.py +++ b/src/mcp/client/stdio.py @@ -77,7 +77,8 @@ class StdioServerParameters(BaseModel): """ The text encoding error handler. - See https://docs.python.org/3/library/codecs.html#codec-base-classes for explanations of possible values + See https://docs.python.org/3/library/codecs.html#codec-base-classes for + explanations of possible values """ @@ -108,7 +109,11 @@ async def stdio_client(server: StdioServerParameters): try: async with read_stream_writer: buffer = "" - async for chunk in TextReceiveStream(process.stdout, encoding=server.encoding, errors=server.encoding_error_handler): + async for chunk in TextReceiveStream( + process.stdout, + encoding=server.encoding, + errors=server.encoding_error_handler, + ): lines = (buffer + chunk).split("\n") buffer = lines.pop() @@ -130,7 +135,12 @@ async def stdio_client(server: StdioServerParameters): async with write_stream_reader: async for message in write_stream_reader: json = message.model_dump_json(by_alias=True, exclude_none=True) - await process.stdin.send((json + "\n").encode(encoding=server.encoding, errors=server.encoding_error_handler)) + await process.stdin.send( + (json + "\n").encode( + encoding=server.encoding, + errors=server.encoding_error_handler, + ) + ) except anyio.ClosedResourceError: await anyio.lowlevel.checkpoint() From 0970ef4ae026c38cab6d010a990b6c38635b982a Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Mon, 30 Dec 2024 10:04:19 -0800 Subject: [PATCH 05/24] Fix SSE server bug with uncaught TypeError --- .../servers/simple-prompt/mcp_simple_prompt/server.py | 9 +++------ .../simple-resource/mcp_simple_resource/server.py | 9 +++------ examples/servers/simple-tool/mcp_simple_tool/server.py | 9 +++------ src/mcp/server/fastmcp/server.py | 9 +++------ src/mcp/server/sse.py | 2 +- 5 files changed, 13 insertions(+), 25 deletions(-) diff --git a/examples/servers/simple-prompt/mcp_simple_prompt/server.py b/examples/servers/simple-prompt/mcp_simple_prompt/server.py index 8427b12..351099f 100644 --- a/examples/servers/simple-prompt/mcp_simple_prompt/server.py +++ b/examples/servers/simple-prompt/mcp_simple_prompt/server.py @@ -90,9 +90,9 @@ def main(port: int, transport: str) -> int: if transport == "sse": from mcp.server.sse import SseServerTransport from starlette.applications import Starlette - from starlette.routing import Route + from starlette.routing import Route, Mount - sse = SseServerTransport("/messages") + sse = SseServerTransport("/messages/") async def handle_sse(request): async with sse.connect_sse( @@ -102,14 +102,11 @@ def main(port: int, transport: str) -> int: streams[0], streams[1], app.create_initialization_options() ) - async def handle_messages(request): - await sse.handle_post_message(request.scope, request.receive, request._send) - starlette_app = Starlette( debug=True, routes=[ Route("/sse", endpoint=handle_sse), - Route("/messages", endpoint=handle_messages, methods=["POST"]), + Mount("/messages/", app=sse.handle_post_message) ], ) diff --git a/examples/servers/simple-resource/mcp_simple_resource/server.py b/examples/servers/simple-resource/mcp_simple_resource/server.py index 9864fc5..51f2933 100644 --- a/examples/servers/simple-resource/mcp_simple_resource/server.py +++ b/examples/servers/simple-resource/mcp_simple_resource/server.py @@ -47,9 +47,9 @@ def main(port: int, transport: str) -> int: if transport == "sse": from mcp.server.sse import SseServerTransport from starlette.applications import Starlette - from starlette.routing import Route + from starlette.routing import Route, Mount - sse = SseServerTransport("/messages") + sse = SseServerTransport("/messages/") async def handle_sse(request): async with sse.connect_sse( @@ -59,14 +59,11 @@ def main(port: int, transport: str) -> int: streams[0], streams[1], app.create_initialization_options() ) - async def handle_messages(request): - await sse.handle_post_message(request.scope, request.receive, request._send) - starlette_app = Starlette( debug=True, routes=[ Route("/sse", endpoint=handle_sse), - Route("/messages", endpoint=handle_messages, methods=["POST"]), + Mount("/messages/", app=sse.handle_post_message), ], ) diff --git a/examples/servers/simple-tool/mcp_simple_tool/server.py b/examples/servers/simple-tool/mcp_simple_tool/server.py index 79b0b8b..039af9e 100644 --- a/examples/servers/simple-tool/mcp_simple_tool/server.py +++ b/examples/servers/simple-tool/mcp_simple_tool/server.py @@ -60,9 +60,9 @@ def main(port: int, transport: str) -> int: if transport == "sse": from mcp.server.sse import SseServerTransport from starlette.applications import Starlette - from starlette.routing import Route + from starlette.routing import Route, Mount - sse = SseServerTransport("/messages") + sse = SseServerTransport("/messages/") async def handle_sse(request): async with sse.connect_sse( @@ -72,14 +72,11 @@ def main(port: int, transport: str) -> int: streams[0], streams[1], app.create_initialization_options() ) - async def handle_messages(request): - await sse.handle_post_message(request.scope, request.receive, request._send) - starlette_app = Starlette( debug=True, routes=[ Route("/sse", endpoint=handle_sse), - Route("/messages", endpoint=handle_messages, methods=["POST"]), + Mount("/messages/", app=sse.handle_post_message), ], ) diff --git a/src/mcp/server/fastmcp/server.py b/src/mcp/server/fastmcp/server.py index f090928..5b65707 100644 --- a/src/mcp/server/fastmcp/server.py +++ b/src/mcp/server/fastmcp/server.py @@ -423,9 +423,9 @@ class FastMCP: async def run_sse_async(self) -> None: """Run the server using SSE transport.""" from starlette.applications import Starlette - from starlette.routing import Route + from starlette.routing import Route, Mount - sse = SseServerTransport("/messages") + sse = SseServerTransport("/messages/") async def handle_sse(request): async with sse.connect_sse( @@ -437,14 +437,11 @@ class FastMCP: self._mcp_server.create_initialization_options(), ) - async def handle_messages(request): - await sse.handle_post_message(request.scope, request.receive, request._send) - starlette_app = Starlette( debug=self.settings.debug, routes=[ Route("/sse", endpoint=handle_sse), - Route("/messages", endpoint=handle_messages, methods=["POST"]), + Mount("/messages/", app=sse.handle_post_message), ], ) diff --git a/src/mcp/server/sse.py b/src/mcp/server/sse.py index 3062b32..0f50023 100644 --- a/src/mcp/server/sse.py +++ b/src/mcp/server/sse.py @@ -11,7 +11,7 @@ Example usage: # Create Starlette routes for SSE and message handling routes = [ Route("/sse", endpoint=handle_sse), - Route("/messages", endpoint=handle_messages, methods=["POST"]) + Mount("/messages", endpoint=handle_messages, methods=["POST"]) ] # Define handler functions From 1134421a3af7950e609f2b595bf18296d4626b16 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Mon, 30 Dec 2024 10:08:57 -0800 Subject: [PATCH 06/24] Fix inconsistencies in examples --- examples/servers/simple-prompt/mcp_simple_prompt/server.py | 2 +- src/mcp/server/sse.py | 5 +---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/examples/servers/simple-prompt/mcp_simple_prompt/server.py b/examples/servers/simple-prompt/mcp_simple_prompt/server.py index 351099f..89b0eea 100644 --- a/examples/servers/simple-prompt/mcp_simple_prompt/server.py +++ b/examples/servers/simple-prompt/mcp_simple_prompt/server.py @@ -106,7 +106,7 @@ def main(port: int, transport: str) -> int: debug=True, routes=[ Route("/sse", endpoint=handle_sse), - Mount("/messages/", app=sse.handle_post_message) + Mount("/messages/", app=sse.handle_post_message), ], ) diff --git a/src/mcp/server/sse.py b/src/mcp/server/sse.py index 0f50023..96410bb 100644 --- a/src/mcp/server/sse.py +++ b/src/mcp/server/sse.py @@ -11,7 +11,7 @@ Example usage: # Create Starlette routes for SSE and message handling routes = [ Route("/sse", endpoint=handle_sse), - Mount("/messages", endpoint=handle_messages, methods=["POST"]) + Mount("/messages", app=sse.handle_post_message), ] # Define handler functions @@ -23,9 +23,6 @@ Example usage: streams[0], streams[1], app.create_initialization_options() ) - async def handle_messages(request): - await sse.handle_post_message(request.scope, request.receive, request._send) - # Create and run Starlette app starlette_app = Starlette(routes=routes) uvicorn.run(starlette_app, host="0.0.0.0", port=port) From 247aec9332665226f7b5a5aae348c37419a722bc Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Mon, 30 Dec 2024 10:09:42 -0800 Subject: [PATCH 07/24] Update paths in examples --- src/mcp/server/sse.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/mcp/server/sse.py b/src/mcp/server/sse.py index 96410bb..0127753 100644 --- a/src/mcp/server/sse.py +++ b/src/mcp/server/sse.py @@ -6,12 +6,12 @@ This module implements a Server-Sent Events (SSE) transport layer for MCP server Example usage: ``` # Create an SSE transport at an endpoint - sse = SseServerTransport("/messages") + sse = SseServerTransport("/messages/") # Create Starlette routes for SSE and message handling routes = [ Route("/sse", endpoint=handle_sse), - Mount("/messages", app=sse.handle_post_message), + Mount("/messages/", app=sse.handle_post_message), ] # Define handler functions From 937c640f04d9bc74bc3b75419c7a93e22ee0719b Mon Sep 17 00:00:00 2001 From: restlessronin <88921269+restlessronin@users.noreply.github.com> Date: Thu, 5 Dec 2024 10:30:01 +0530 Subject: [PATCH 08/24] feat: add version string parameter to 'create_initialization_options' --- README.md | 9 +-------- src/mcp/server/__init__.py | 3 ++- src/mcp/server/sse.py | 3 ++- 3 files changed, 5 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 4ef08cf..20fd03e 100644 --- a/README.md +++ b/README.md @@ -135,14 +135,7 @@ async def run(): await server.run( read_stream, write_stream, - InitializationOptions( - server_name="example", - server_version="0.1.0", - capabilities=server.get_capabilities( - notification_options=NotificationOptions(), - experimental_capabilities={}, - ) - ) + server.create_initialization_options("0.1.0") ) if __name__ == "__main__": diff --git a/src/mcp/server/__init__.py b/src/mcp/server/__init__.py index a0dd033..212f011 100644 --- a/src/mcp/server/__init__.py +++ b/src/mcp/server/__init__.py @@ -114,6 +114,7 @@ class Server: def create_initialization_options( self, + version: str | None = None, notification_options: NotificationOptions | None = None, experimental_capabilities: dict[str, dict[str, Any]] | None = None, ) -> InitializationOptions: @@ -133,7 +134,7 @@ class Server: return InitializationOptions( server_name=self.name, - server_version=pkg_version("mcp"), + server_version=version if version else pkg_version("mcp"), capabilities=self.get_capabilities( notification_options or NotificationOptions(), experimental_capabilities or {}, diff --git a/src/mcp/server/sse.py b/src/mcp/server/sse.py index 3062b32..765c0eb 100644 --- a/src/mcp/server/sse.py +++ b/src/mcp/server/sse.py @@ -19,8 +19,9 @@ Example usage: async with sse.connect_sse( request.scope, request.receive, request._send ) as streams: + # Pass user visible version string, egs. 0.1.0 await app.run( - streams[0], streams[1], app.create_initialization_options() + streams[0], streams[1], app.create_initialization_options("0.1.0") ) async def handle_messages(request): From 6f108f7f7c0bc151a1c8a0c1d2830834f8231ac2 Mon Sep 17 00:00:00 2001 From: restlessronin <88921269+restlessronin@users.noreply.github.com> Date: Thu, 5 Dec 2024 21:27:58 +0530 Subject: [PATCH 09/24] doc: added comment to README example --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 20fd03e..87eb245 100644 --- a/README.md +++ b/README.md @@ -135,7 +135,7 @@ async def run(): await server.run( read_stream, write_stream, - server.create_initialization_options("0.1.0") + server.create_initialization_options("0.1.0") # user visible version number ) if __name__ == "__main__": From 34a257147bb7375846ea2ecb02afd355605e7ec4 Mon Sep 17 00:00:00 2001 From: restlessronin <88921269+restlessronin@users.noreply.github.com> Date: Tue, 10 Dec 2024 20:51:36 +0530 Subject: [PATCH 10/24] chore: revert doc changes --- README.md | 9 ++++++++- src/mcp/server/sse.py | 3 +-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 87eb245..4ef08cf 100644 --- a/README.md +++ b/README.md @@ -135,7 +135,14 @@ async def run(): await server.run( read_stream, write_stream, - server.create_initialization_options("0.1.0") # user visible version number + InitializationOptions( + server_name="example", + server_version="0.1.0", + capabilities=server.get_capabilities( + notification_options=NotificationOptions(), + experimental_capabilities={}, + ) + ) ) if __name__ == "__main__": diff --git a/src/mcp/server/sse.py b/src/mcp/server/sse.py index 765c0eb..3062b32 100644 --- a/src/mcp/server/sse.py +++ b/src/mcp/server/sse.py @@ -19,9 +19,8 @@ Example usage: async with sse.connect_sse( request.scope, request.receive, request._send ) as streams: - # Pass user visible version string, egs. 0.1.0 await app.run( - streams[0], streams[1], app.create_initialization_options("0.1.0") + streams[0], streams[1], app.create_initialization_options() ) async def handle_messages(request): From 3de4dc1f13fc4800a8bd4f3e6a52f0d4219f30d0 Mon Sep 17 00:00:00 2001 From: restlessronin <88921269+restlessronin@users.noreply.github.com> Date: Tue, 10 Dec 2024 20:54:52 +0530 Subject: [PATCH 11/24] feat: add version string parameter to Server constructor --- src/mcp/server/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/mcp/server/__init__.py b/src/mcp/server/__init__.py index 212f011..4c5dc04 100644 --- a/src/mcp/server/__init__.py +++ b/src/mcp/server/__init__.py @@ -101,8 +101,9 @@ class NotificationOptions: class Server: - def __init__(self, name: str): + def __init__(self, name: str, version: str | None = None): self.name = name + self.version = version self.request_handlers: dict[ type, Callable[..., Awaitable[types.ServerResult]] ] = { @@ -114,7 +115,6 @@ class Server: def create_initialization_options( self, - version: str | None = None, notification_options: NotificationOptions | None = None, experimental_capabilities: dict[str, dict[str, Any]] | None = None, ) -> InitializationOptions: @@ -134,7 +134,7 @@ class Server: return InitializationOptions( server_name=self.name, - server_version=version if version else pkg_version("mcp"), + server_version=self.version if self.version else pkg_version("mcp"), capabilities=self.get_capabilities( notification_options or NotificationOptions(), experimental_capabilities or {}, From b89cabc7df5dfa48f893775a07872729532201b5 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Thu, 2 Jan 2025 09:30:18 +0000 Subject: [PATCH 12/24] fix: Add constructor for McpError to allow setting field Backport of #116 by @allenporter to v1.1.x branch. Fixes an issue where exception handling code fails with AttributeError when accessing the error field of McpError. --- src/mcp/shared/exceptions.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/mcp/shared/exceptions.py b/src/mcp/shared/exceptions.py index d8855b8..97a1c09 100644 --- a/src/mcp/shared/exceptions.py +++ b/src/mcp/shared/exceptions.py @@ -7,3 +7,8 @@ class McpError(Exception): """ error: ErrorData + + def __init__(self, error: ErrorData): + """Initialize McpError.""" + super().__init__(error.message) + self.error = error From ae73191b3edadd363f251a4194658d14da18aafe Mon Sep 17 00:00:00 2001 From: David Soria Parra Date: Thu, 2 Jan 2025 16:57:53 +0000 Subject: [PATCH 13/24] fix: Change --env-var short flag from -e to -v to avoid conflict with --with-editable --- src/mcp/cli/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mcp/cli/cli.py b/src/mcp/cli/cli.py index 3e164d9..f30529c 100644 --- a/src/mcp/cli/cli.py +++ b/src/mcp/cli/cli.py @@ -373,7 +373,7 @@ def install( list[str], typer.Option( "--env-var", - "-e", + "-v", help="Environment variables in KEY=VALUE format", ), ] = [], From 73c055f136be4f33591ceb69091cb750135e7bcb Mon Sep 17 00:00:00 2001 From: David Soria Parra Date: Thu, 2 Jan 2025 16:59:26 +0000 Subject: [PATCH 14/24] fix: Update AnyIO stream creation to use type annotation syntax --- tests/server/test_session.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/tests/server/test_session.py b/tests/server/test_session.py index ead18f7..67d7d48 100644 --- a/tests/server/test_session.py +++ b/tests/server/test_session.py @@ -18,12 +18,8 @@ from mcp.types import ( @pytest.mark.anyio async def test_server_session_initialize(): - server_to_client_send, server_to_client_receive = anyio.create_memory_object_stream( - 1, JSONRPCMessage - ) - client_to_server_send, client_to_server_receive = anyio.create_memory_object_stream( - 1, JSONRPCMessage - ) + server_to_client_send, server_to_client_receive = anyio.create_memory_object_stream[JSONRPCMessage](1) + client_to_server_send, client_to_server_receive = anyio.create_memory_object_stream[JSONRPCMessage](1) async def run_client(client: ClientSession): async for message in client_session.incoming_messages: From 43796bfa241246dc454e8009c1f10da428f62c79 Mon Sep 17 00:00:00 2001 From: David Soria Parra Date: Fri, 3 Jan 2025 14:50:48 +0000 Subject: [PATCH 15/24] test: Add test for non-ASCII character handling in FastMCP tools --- tests/server/fastmcp/test_server.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/tests/server/fastmcp/test_server.py b/tests/server/fastmcp/test_server.py index f5aa4cd..5d2380f 100644 --- a/tests/server/fastmcp/test_server.py +++ b/tests/server/fastmcp/test_server.py @@ -30,6 +30,29 @@ class TestServer: mcp = FastMCP() assert mcp.name == "FastMCP" + @pytest.mark.anyio + async def test_non_ascii_description(self): + """Test that FastMCP handles non-ASCII characters in descriptions correctly""" + mcp = FastMCP() + + @mcp.tool(description="🌟 This tool uses emojis and UTF-8 characters: á é í ó ú ñ 漢字 🎉") + def hello_world(name: str = "世界") -> str: + return f"¡Hola, {name}! 👋" + + async with client_session(mcp._mcp_server) as client: + tools = await client.list_tools() + assert len(tools.tools) == 1 + tool = tools.tools[0] + assert "🌟" in tool.description + assert "漢字" in tool.description + assert "🎉" in tool.description + + result = await client.call_tool("hello_world", {}) + assert len(result.content) == 1 + content = result.content[0] + assert isinstance(content, TextContent) + assert "¡Hola, 世界! 👋" == content.text + @pytest.mark.anyio async def test_add_tool_decorator(self): mcp = FastMCP() From ed993166e4b424dcbc460ce39b1c9e3b83dff8e8 Mon Sep 17 00:00:00 2001 From: David Soria Parra Date: Fri, 3 Jan 2025 14:51:22 +0000 Subject: [PATCH 16/24] example: Add FastMCP example with Unicode characters and emojis --- examples/fastmcp/unicode_example.py | 59 +++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 examples/fastmcp/unicode_example.py diff --git a/examples/fastmcp/unicode_example.py b/examples/fastmcp/unicode_example.py new file mode 100644 index 0000000..401b788 --- /dev/null +++ b/examples/fastmcp/unicode_example.py @@ -0,0 +1,59 @@ +""" +Example FastMCP server that uses Unicode characters in various places to help test +Unicode handling in tools and inspectors. +""" + +from mcp.server.fastmcp import FastMCP + +mcp = FastMCP() + + +@mcp.tool(description="🌟 A tool that uses various Unicode characters in its description: á é í ó ú ñ 漢字 🎉") +def hello_unicode(name: str = "世界", greeting: str = "¡Hola") -> str: + """ + A simple tool that demonstrates Unicode handling in: + - Tool description (emojis, accents, CJK characters) + - Parameter defaults (CJK characters) + - Return values (Spanish punctuation, emojis) + """ + return f"{greeting}, {name}! 👋" + + +@mcp.tool(description="🎨 Tool that returns a list of emoji categories") +def list_emoji_categories() -> list[str]: + """Returns a list of emoji categories with emoji examples.""" + return [ + "😀 Smileys & Emotion", + "👋 People & Body", + "🐶 Animals & Nature", + "🍎 Food & Drink", + "⚽ Activities", + "🌍 Travel & Places", + "💡 Objects", + "❤️ Symbols", + "🚩 Flags" + ] + + +@mcp.tool(description="🔤 Tool that returns text in different scripts") +def multilingual_hello() -> str: + """Returns hello in different scripts and writing systems.""" + return "\n".join([ + "English: Hello!", + "Spanish: ¡Hola!", + "French: Bonjour!", + "German: Grüß Gott!", + "Russian: Привет!", + "Greek: Γεια σας!", + "Hebrew: !שָׁלוֹם", + "Arabic: !مرحبا", + "Hindi: नमस्ते!", + "Chinese: 你好!", + "Japanese: こんにちは!", + "Korean: 안녕하세요!", + "Thai: สวัสดี!", + ]) + + +if __name__ == "__main__": + mcp.run() \ No newline at end of file From 79ec8dccdb62e1a7d7e2de711af185236c5f87fb Mon Sep 17 00:00:00 2001 From: David Soria Parra Date: Fri, 3 Jan 2025 15:19:57 +0000 Subject: [PATCH 17/24] test: Add test for unlimited tool listing (issue #100) --- tests/issues/test_100_tool_listing.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 tests/issues/test_100_tool_listing.py diff --git a/tests/issues/test_100_tool_listing.py b/tests/issues/test_100_tool_listing.py new file mode 100644 index 0000000..2810a02 --- /dev/null +++ b/tests/issues/test_100_tool_listing.py @@ -0,0 +1,27 @@ +import pytest +from mcp.server.fastmcp import FastMCP + +pytestmark = pytest.mark.anyio + +async def test_list_tools_returns_all_tools(): + mcp = FastMCP("TestTools") + + # Create 100 tools with unique names + num_tools = 100 + for i in range(num_tools): + @mcp.tool(name=f"tool_{i}") + def dummy_tool_func(): + f"""Tool number {i}""" + return i + globals()[f'dummy_tool_{i}'] = dummy_tool_func # Keep reference to avoid garbage collection + + # Get all tools + tools = await mcp.list_tools() + + # Verify we get all tools + assert len(tools) == num_tools, f"Expected {num_tools} tools, but got {len(tools)}" + + # Verify each tool is unique and has the correct name + tool_names = [tool.name for tool in tools] + expected_names = [f"tool_{i}" for i in range(num_tools)] + assert sorted(tool_names) == sorted(expected_names), "Tool names don't match expected names" \ No newline at end of file From 2d8f08d6482a56f6d877cba8c6e2c66c438b4ceb Mon Sep 17 00:00:00 2001 From: David Soria Parra Date: Fri, 3 Jan 2025 15:23:58 +0000 Subject: [PATCH 18/24] fix: Address formatting and type checking issues --- examples/fastmcp/unicode_example.py | 41 +++++++++++++++------------ tests/client/test_config.py | 24 +++++++--------- tests/issues/test_100_tool_listing.py | 19 +++++++++---- tests/server/fastmcp/test_server.py | 5 +++- tests/server/test_session.py | 8 ++++-- 5 files changed, 56 insertions(+), 41 deletions(-) diff --git a/examples/fastmcp/unicode_example.py b/examples/fastmcp/unicode_example.py index 401b788..a69f586 100644 --- a/examples/fastmcp/unicode_example.py +++ b/examples/fastmcp/unicode_example.py @@ -8,7 +8,10 @@ from mcp.server.fastmcp import FastMCP mcp = FastMCP() -@mcp.tool(description="🌟 A tool that uses various Unicode characters in its description: á é í ó ú ñ 漢字 🎉") +@mcp.tool( + description="🌟 A tool that uses various Unicode characters in its description: " + "á é í ó ú ñ 漢字 🎉" +) def hello_unicode(name: str = "世界", greeting: str = "¡Hola") -> str: """ A simple tool that demonstrates Unicode handling in: @@ -31,29 +34,31 @@ def list_emoji_categories() -> list[str]: "🌍 Travel & Places", "💡 Objects", "❤️ Symbols", - "🚩 Flags" + "🚩 Flags", ] @mcp.tool(description="🔤 Tool that returns text in different scripts") def multilingual_hello() -> str: """Returns hello in different scripts and writing systems.""" - return "\n".join([ - "English: Hello!", - "Spanish: ¡Hola!", - "French: Bonjour!", - "German: Grüß Gott!", - "Russian: Привет!", - "Greek: Γεια σας!", - "Hebrew: !שָׁלוֹם", - "Arabic: !مرحبا", - "Hindi: नमस्ते!", - "Chinese: 你好!", - "Japanese: こんにちは!", - "Korean: 안녕하세요!", - "Thai: สวัสดี!", - ]) + return "\n".join( + [ + "English: Hello!", + "Spanish: ¡Hola!", + "French: Bonjour!", + "German: Grüß Gott!", + "Russian: Привет!", + "Greek: Γεια σας!", + "Hebrew: !שָׁלוֹם", + "Arabic: !مرحبا", + "Hindi: नमस्ते!", + "Chinese: 你好!", + "Japanese: こんにちは!", + "Korean: 안녕하세요!", + "Thai: สวัสดี!", + ] + ) if __name__ == "__main__": - mcp.run() \ No newline at end of file + mcp.run() diff --git a/tests/client/test_config.py b/tests/client/test_config.py index 2a8273e..b8371e4 100644 --- a/tests/client/test_config.py +++ b/tests/client/test_config.py @@ -1,6 +1,5 @@ import json import subprocess -from pathlib import Path from unittest.mock import patch import pytest @@ -15,42 +14,39 @@ def temp_config_dir(tmp_path): config_dir.mkdir() return config_dir + @pytest.fixture def mock_config_path(temp_config_dir): """Mock get_claude_config_path to return our temporary directory.""" - with patch('mcp.cli.claude.get_claude_config_path', return_value=temp_config_dir): + with patch("mcp.cli.claude.get_claude_config_path", return_value=temp_config_dir): yield temp_config_dir + def test_command_execution(mock_config_path): """Test that the generated command can actually be executed.""" # Setup server_name = "test_server" file_spec = "test_server.py:app" - + # Update config success = update_claude_config( file_spec=file_spec, server_name=server_name, ) assert success - + # Read the generated config config_file = mock_config_path / "claude_desktop_config.json" config = json.loads(config_file.read_text()) - + # Get the command and args server_config = config["mcpServers"][server_name] command = server_config["command"] args = server_config["args"] test_args = [command] + args + ["--help"] - - result = subprocess.run( - test_args, - capture_output=True, - text=True, - timeout=5 - ) - + + result = subprocess.run(test_args, capture_output=True, text=True, timeout=5) + assert result.returncode == 0 - assert "usage" in result.stdout.lower() \ No newline at end of file + assert "usage" in result.stdout.lower() diff --git a/tests/issues/test_100_tool_listing.py b/tests/issues/test_100_tool_listing.py index 2810a02..4bc6d63 100644 --- a/tests/issues/test_100_tool_listing.py +++ b/tests/issues/test_100_tool_listing.py @@ -3,25 +3,32 @@ from mcp.server.fastmcp import FastMCP pytestmark = pytest.mark.anyio + async def test_list_tools_returns_all_tools(): mcp = FastMCP("TestTools") - + # Create 100 tools with unique names num_tools = 100 for i in range(num_tools): + @mcp.tool(name=f"tool_{i}") def dummy_tool_func(): f"""Tool number {i}""" return i - globals()[f'dummy_tool_{i}'] = dummy_tool_func # Keep reference to avoid garbage collection - + + globals()[f"dummy_tool_{i}"] = ( + dummy_tool_func # Keep reference to avoid garbage collection + ) + # Get all tools tools = await mcp.list_tools() - + # Verify we get all tools assert len(tools) == num_tools, f"Expected {num_tools} tools, but got {len(tools)}" - + # Verify each tool is unique and has the correct name tool_names = [tool.name for tool in tools] expected_names = [f"tool_{i}" for i in range(num_tools)] - assert sorted(tool_names) == sorted(expected_names), "Tool names don't match expected names" \ No newline at end of file + assert sorted(tool_names) == sorted( + expected_names + ), "Tool names don't match expected names" diff --git a/tests/server/fastmcp/test_server.py b/tests/server/fastmcp/test_server.py index 5d2380f..63841af 100644 --- a/tests/server/fastmcp/test_server.py +++ b/tests/server/fastmcp/test_server.py @@ -35,7 +35,9 @@ class TestServer: """Test that FastMCP handles non-ASCII characters in descriptions correctly""" mcp = FastMCP() - @mcp.tool(description="🌟 This tool uses emojis and UTF-8 characters: á é í ó ú ñ 漢字 🎉") + @mcp.tool( + description="🌟 This tool uses emojis and UTF-8 characters: á é í ó ú ñ 漢字 🎉" + ) def hello_world(name: str = "世界") -> str: return f"¡Hola, {name}! 👋" @@ -43,6 +45,7 @@ class TestServer: tools = await client.list_tools() assert len(tools.tools) == 1 tool = tools.tools[0] + assert tool.description is not None assert "🌟" in tool.description assert "漢字" in tool.description assert "🎉" in tool.description diff --git a/tests/server/test_session.py b/tests/server/test_session.py index 67d7d48..333196c 100644 --- a/tests/server/test_session.py +++ b/tests/server/test_session.py @@ -18,8 +18,12 @@ from mcp.types import ( @pytest.mark.anyio async def test_server_session_initialize(): - server_to_client_send, server_to_client_receive = anyio.create_memory_object_stream[JSONRPCMessage](1) - client_to_server_send, client_to_server_receive = anyio.create_memory_object_stream[JSONRPCMessage](1) + server_to_client_send, server_to_client_receive = anyio.create_memory_object_stream[ + JSONRPCMessage + ](1) + client_to_server_send, client_to_server_receive = anyio.create_memory_object_stream[ + JSONRPCMessage + ](1) async def run_client(client: ClientSession): async for message in client_session.incoming_messages: From 1bbd27d4a50a56afc59be97473f5f3448bf1303e Mon Sep 17 00:00:00 2001 From: David Soria Parra Date: Fri, 3 Jan 2025 15:26:25 +0000 Subject: [PATCH 19/24] docs: Add learnings about tool usage --- CLAUDE.md | 56 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..c501926 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,56 @@ +# Tool Usage Learnings + +This file is intended to be used by an LLM such as Claude. + +## UV Package Manager + +- Use `uv run` to run Python tools without activating virtual environments +- For formatting: `uv run ruff format .` +- For type checking: `uv run pyright` +- For upgrading packages: + - `uv add --dev package --upgrade-package package` to upgrade a specific package + - Don't use `@latest` syntax - it doesn't work + - Be careful with `uv pip install` as it may downgrade packages + +## Git and GitHub CLI + +- When using gh CLI for PRs, always quote title and body: + ```bash + gh pr create --title "\"my title\"" --body "\"my body\"" + ``` +- For git commits, use double quotes and escape inner quotes: + ```bash + git commit -am "\"fix: my commit message\"" + ``` + +## Python Tools + +### Ruff +- Handles both formatting and linting +- For formatting: `uv run ruff format .` +- For checking: `uv run ruff check .` +- Common issues: + - Line length (default 88 chars) + - Import sorting + - Unused imports + +### Pyright +- Type checker +- Run with: `uv run pyright` +- Version warnings can be ignored if type checking passes +- Common issues: + - Optional types need explicit None checks + - String operations need type narrowing + +## Best Practices + +1. Always check git status and diff before committing +2. Run formatters before type checkers +3. When fixing CI: + - Start with formatting issues + - Then fix type errors + - Then address any remaining linting issues +4. For type errors: + - Get full context around error lines + - Consider optional types + - Add type narrowing checks when needed From 118ddcc9ac6ce999143785c02f53566d24e09e13 Mon Sep 17 00:00:00 2001 From: David Soria Parra Date: Fri, 3 Jan 2025 15:30:49 +0000 Subject: [PATCH 20/24] feat: upgrade dev dependencies --- pyproject.toml | 6 +++--- uv.lock | 57 +++++++++++++++++++++++++------------------------- 2 files changed, 32 insertions(+), 31 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 8e487f4..83a37da 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,9 +42,9 @@ mcp = "mcp.cli:app [cli]" [tool.uv] resolution = "lowest-direct" dev-dependencies = [ - "pyright>=1.1.378", - "pytest>=8.3.3", - "ruff>=0.8.1", + "pyright>=1.1.391", + "pytest>=8.3.4", + "ruff>=0.8.5", "trio>=0.26.2", "pytest-flakefinder>=1.1.0", "pytest-xdist>=3.6.1", diff --git a/uv.lock b/uv.lock index e05eee4..1eec17f 100644 --- a/uv.lock +++ b/uv.lock @@ -240,11 +240,11 @@ requires-dist = [ [package.metadata.requires-dev] dev = [ - { name = "pyright", specifier = ">=1.1.378" }, - { name = "pytest", specifier = ">=8.3.3" }, + { name = "pyright", specifier = ">=1.1.391" }, + { name = "pytest", specifier = ">=8.3.4" }, { name = "pytest-flakefinder", specifier = ">=1.1.0" }, { name = "pytest-xdist", specifier = ">=3.6.1" }, - { name = "ruff", specifier = ">=0.8.1" }, + { name = "ruff", specifier = ">=0.8.5" }, { name = "trio", specifier = ">=0.26.2" }, ] @@ -517,19 +517,20 @@ wheels = [ [[package]] name = "pyright" -version = "1.1.378" +version = "1.1.391" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "nodeenv" }, + { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3d/f0/e8aa5555410d88f898bef04da2102b0a9bf144658c98d34872e91621ced2/pyright-1.1.378.tar.gz", hash = "sha256:78a043be2876d12d0af101d667e92c7734f3ebb9db71dccc2c220e7e7eb89ca2", size = 17486 } +sdist = { url = "https://files.pythonhosted.org/packages/11/05/4ea52a8a45cc28897edb485b4102d37cbfd5fce8445d679cdeb62bfad221/pyright-1.1.391.tar.gz", hash = "sha256:66b2d42cdf5c3cbab05f2f4b76e8bec8aa78e679bfa0b6ad7b923d9e027cadb2", size = 21965 } wheels = [ - { url = "https://files.pythonhosted.org/packages/38/c6/f0d4bc20c13b20cecfbf13c699477c825e45767f1dc5068137323f86e495/pyright-1.1.378-py3-none-any.whl", hash = "sha256:8853776138b01bc284da07ac481235be7cc89d3176b073d2dba73636cb95be79", size = 18222 }, + { url = "https://files.pythonhosted.org/packages/ad/89/66f49552fbeb21944c8077d11834b2201514a56fd1b7747ffff9630f1bd9/pyright-1.1.391-py3-none-any.whl", hash = "sha256:54fa186f8b3e8a55a44ebfa842636635688670c6896dcf6cf4a7fc75062f4d15", size = 18579 }, ] [[package]] name = "pytest" -version = "8.3.3" +version = "8.3.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, @@ -539,9 +540,9 @@ dependencies = [ { name = "pluggy" }, { name = "tomli", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/8b/6c/62bbd536103af674e227c41a8f3dcd022d591f6eed5facb5a0f31ee33bbc/pytest-8.3.3.tar.gz", hash = "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181", size = 1442487 } +sdist = { url = "https://files.pythonhosted.org/packages/05/35/30e0d83068951d90a01852cb1cef56e5d8a09d20c7f511634cc2f7e0372a/pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761", size = 1445919 } wheels = [ - { url = "https://files.pythonhosted.org/packages/6b/77/7440a06a8ead44c7757a64362dd22df5760f9b12dc5f11b6188cd2fc27a0/pytest-8.3.3-py3-none-any.whl", hash = "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2", size = 342341 }, + { url = "https://files.pythonhosted.org/packages/11/92/76a1c94d3afee238333bc0a42b82935dd8f9cf8ce9e336ff87ee14d9e1cf/pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6", size = 343083 }, ] [[package]] @@ -594,27 +595,27 @@ wheels = [ [[package]] name = "ruff" -version = "0.8.1" +version = "0.8.5" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/95/d0/8ff5b189d125f4260f2255d143bf2fa413b69c2610c405ace7a0a8ec81ec/ruff-0.8.1.tar.gz", hash = "sha256:3583db9a6450364ed5ca3f3b4225958b24f78178908d5c4bc0f46251ccca898f", size = 3313222 } +sdist = { url = "https://files.pythonhosted.org/packages/25/5d/4b5403f3e89837decfd54c51bea7f94b7d3fae77e08858603d0e04d7ad17/ruff-0.8.5.tar.gz", hash = "sha256:1098d36f69831f7ff2a1da3e6407d5fbd6dfa2559e4f74ff2d260c5588900317", size = 3454835 } wheels = [ - { url = "https://files.pythonhosted.org/packages/a2/d6/1a6314e568db88acdbb5121ed53e2c52cebf3720d3437a76f82f923bf171/ruff-0.8.1-py3-none-linux_armv6l.whl", hash = "sha256:fae0805bd514066f20309f6742f6ee7904a773eb9e6c17c45d6b1600ca65c9b5", size = 10532605 }, - { url = "https://files.pythonhosted.org/packages/89/a8/a957a8812e31facffb6a26a30be0b5b4af000a6e30c7d43a22a5232a3398/ruff-0.8.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b8a4f7385c2285c30f34b200ca5511fcc865f17578383db154e098150ce0a087", size = 10278243 }, - { url = "https://files.pythonhosted.org/packages/a8/23/9db40fa19c453fabf94f7a35c61c58f20e8200b4734a20839515a19da790/ruff-0.8.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:cd054486da0c53e41e0086e1730eb77d1f698154f910e0cd9e0d64274979a209", size = 9917739 }, - { url = "https://files.pythonhosted.org/packages/e2/a0/6ee2d949835d5701d832fc5acd05c0bfdad5e89cfdd074a171411f5ccad5/ruff-0.8.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2029b8c22da147c50ae577e621a5bfbc5d1fed75d86af53643d7a7aee1d23871", size = 10779153 }, - { url = "https://files.pythonhosted.org/packages/7a/25/9c11dca9404ef1eb24833f780146236131a3c7941de394bc356912ef1041/ruff-0.8.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2666520828dee7dfc7e47ee4ea0d928f40de72056d929a7c5292d95071d881d1", size = 10304387 }, - { url = "https://files.pythonhosted.org/packages/c8/b9/84c323780db1b06feae603a707d82dbbd85955c8c917738571c65d7d5aff/ruff-0.8.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:333c57013ef8c97a53892aa56042831c372e0bb1785ab7026187b7abd0135ad5", size = 11360351 }, - { url = "https://files.pythonhosted.org/packages/6b/e1/9d4bbb2ace7aad14ded20e4674a48cda5b902aed7a1b14e6b028067060c4/ruff-0.8.1-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:288326162804f34088ac007139488dcb43de590a5ccfec3166396530b58fb89d", size = 12022879 }, - { url = "https://files.pythonhosted.org/packages/75/28/752ff6120c0e7f9981bc4bc275d540c7f36db1379ba9db9142f69c88db21/ruff-0.8.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b12c39b9448632284561cbf4191aa1b005882acbc81900ffa9f9f471c8ff7e26", size = 11610354 }, - { url = "https://files.pythonhosted.org/packages/ba/8c/967b61c2cc8ebd1df877607fbe462bc1e1220b4a30ae3352648aec8c24bd/ruff-0.8.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:364e6674450cbac8e998f7b30639040c99d81dfb5bbc6dfad69bc7a8f916b3d1", size = 12813976 }, - { url = "https://files.pythonhosted.org/packages/7f/29/e059f945d6bd2d90213387b8c360187f2fefc989ddcee6bbf3c241329b92/ruff-0.8.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b22346f845fec132aa39cd29acb94451d030c10874408dbf776af3aaeb53284c", size = 11154564 }, - { url = "https://files.pythonhosted.org/packages/55/47/cbd05e5a62f3fb4c072bc65c1e8fd709924cad1c7ec60a1000d1e4ee8307/ruff-0.8.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:b2f2f7a7e7648a2bfe6ead4e0a16745db956da0e3a231ad443d2a66a105c04fa", size = 10760604 }, - { url = "https://files.pythonhosted.org/packages/bb/ee/4c3981c47147c72647a198a94202633130cfda0fc95cd863a553b6f65c6a/ruff-0.8.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:adf314fc458374c25c5c4a4a9270c3e8a6a807b1bec018cfa2813d6546215540", size = 10391071 }, - { url = "https://files.pythonhosted.org/packages/6b/e6/083eb61300214590b188616a8ac6ae1ef5730a0974240fb4bec9c17de78b/ruff-0.8.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:a885d68342a231b5ba4d30b8c6e1b1ee3a65cf37e3d29b3c74069cdf1ee1e3c9", size = 10896657 }, - { url = "https://files.pythonhosted.org/packages/77/bd/aacdb8285d10f1b943dbeb818968efca35459afc29f66ae3bd4596fbf954/ruff-0.8.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:d2c16e3508c8cc73e96aa5127d0df8913d2290098f776416a4b157657bee44c5", size = 11228362 }, - { url = "https://files.pythonhosted.org/packages/39/72/fcb7ad41947f38b4eaa702aca0a361af0e9c2bf671d7fd964480670c297e/ruff-0.8.1-py3-none-win32.whl", hash = "sha256:93335cd7c0eaedb44882d75a7acb7df4b77cd7cd0d2255c93b28791716e81790", size = 8803476 }, - { url = "https://files.pythonhosted.org/packages/e4/ea/cae9aeb0f4822c44651c8407baacdb2e5b4dcd7b31a84e1c5df33aa2cc20/ruff-0.8.1-py3-none-win_amd64.whl", hash = "sha256:2954cdbe8dfd8ab359d4a30cd971b589d335a44d444b6ca2cb3d1da21b75e4b6", size = 9614463 }, - { url = "https://files.pythonhosted.org/packages/eb/76/fbb4bd23dfb48fa7758d35b744413b650a9fd2ddd93bca77e30376864414/ruff-0.8.1-py3-none-win_arm64.whl", hash = "sha256:55873cc1a473e5ac129d15eccb3c008c096b94809d693fc7053f588b67822737", size = 8959621 }, + { url = "https://files.pythonhosted.org/packages/73/f8/03391745a703ce11678eb37c48ae89ec60396ea821e9d0bcea7c8e88fd91/ruff-0.8.5-py3-none-linux_armv6l.whl", hash = "sha256:5ad11a5e3868a73ca1fa4727fe7e33735ea78b416313f4368c504dbeb69c0f88", size = 10626889 }, + { url = "https://files.pythonhosted.org/packages/55/74/83bb74a44183b904216f3edfb9995b89830c83aaa6ce84627f74da0e0cf8/ruff-0.8.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:f69ab37771ea7e0715fead8624ec42996d101269a96e31f4d31be6fc33aa19b7", size = 10398233 }, + { url = "https://files.pythonhosted.org/packages/e8/7a/a162a4feb3ef85d594527165e366dde09d7a1e534186ff4ba5d127eda850/ruff-0.8.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:b5462d7804558ccff9c08fe8cbf6c14b7efe67404316696a2dde48297b1925bb", size = 10001843 }, + { url = "https://files.pythonhosted.org/packages/e7/9f/5ee5dcd135411402e35b6ec6a8dfdadbd31c5cd1c36a624d356a38d76090/ruff-0.8.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d56de7220a35607f9fe59f8a6d018e14504f7b71d784d980835e20fc0611cd50", size = 10872507 }, + { url = "https://files.pythonhosted.org/packages/b6/67/db2df2dd4a34b602d7f6ebb1b3744c8157f0d3579973ffc58309c9c272e8/ruff-0.8.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9d99cf80b0429cbebf31cbbf6f24f05a29706f0437c40413d950e67e2d4faca4", size = 10377200 }, + { url = "https://files.pythonhosted.org/packages/fe/ff/fe3a6a73006bced73e60d171d154a82430f61d97e787f511a24bd6302611/ruff-0.8.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b75ac29715ac60d554a049dbb0ef3b55259076181c3369d79466cb130eb5afd", size = 11433155 }, + { url = "https://files.pythonhosted.org/packages/e3/95/c1d1a1fe36658c1f3e1b47e1cd5f688b72d5786695b9e621c2c38399a95e/ruff-0.8.5-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:c9d526a62c9eda211b38463528768fd0ada25dad524cb33c0e99fcff1c67b5dc", size = 12139227 }, + { url = "https://files.pythonhosted.org/packages/1b/fe/644b70d473a27b5112ac7a3428edcc1ce0db775c301ff11aa146f71886e0/ruff-0.8.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:587c5e95007612c26509f30acc506c874dab4c4abbacd0357400bd1aa799931b", size = 11697941 }, + { url = "https://files.pythonhosted.org/packages/00/39/4f83e517ec173e16a47c6d102cd22a1aaebe80e1208a1f2e83ab9a0e4134/ruff-0.8.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:622b82bf3429ff0e346835ec213aec0a04d9730480cbffbb6ad9372014e31bbd", size = 12967686 }, + { url = "https://files.pythonhosted.org/packages/1a/f6/52a2973ff108d74b5da706a573379eea160bece098f7cfa3f35dc4622710/ruff-0.8.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f99be814d77a5dac8a8957104bdd8c359e85c86b0ee0e38dca447cb1095f70fb", size = 11253788 }, + { url = "https://files.pythonhosted.org/packages/ce/1f/3b30f3c65b1303cb8e268ec3b046b77ab21ed8e26921cfc7e8232aa57f2c/ruff-0.8.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:c01c048f9c3385e0fd7822ad0fd519afb282af9cf1778f3580e540629df89725", size = 10860360 }, + { url = "https://files.pythonhosted.org/packages/a5/a8/2a3ea6bacead963f7aeeba0c61815d9b27b0d638e6a74984aa5cc5d27733/ruff-0.8.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:7512e8cb038db7f5db6aae0e24735ff9ea03bb0ed6ae2ce534e9baa23c1dc9ea", size = 10457922 }, + { url = "https://files.pythonhosted.org/packages/17/47/8f9514b670969aab57c5fc826fb500a16aee8feac1bcf8a91358f153a5ba/ruff-0.8.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:762f113232acd5b768d6b875d16aad6b00082add40ec91c927f0673a8ec4ede8", size = 10958347 }, + { url = "https://files.pythonhosted.org/packages/0d/d6/78a9af8209ad99541816d74f01ce678fc01ebb3f37dd7ab8966646dcd92b/ruff-0.8.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:03a90200c5dfff49e4c967b405f27fdfa81594cbb7c5ff5609e42d7fe9680da5", size = 11328882 }, + { url = "https://files.pythonhosted.org/packages/54/77/5c8072ec7afdfdf42c7a4019044486a2b6c85ee73617f8875ec94b977fed/ruff-0.8.5-py3-none-win32.whl", hash = "sha256:8710ffd57bdaa6690cbf6ecff19884b8629ec2a2a2a2f783aa94b1cc795139ed", size = 8802515 }, + { url = "https://files.pythonhosted.org/packages/bc/b6/47d2b06784de8ae992c45cceb2a30f3f205b3236a629d7ca4c0c134839a2/ruff-0.8.5-py3-none-win_amd64.whl", hash = "sha256:4020d8bf8d3a32325c77af452a9976a9ad6455773bcb94991cf15bd66b347e47", size = 9684231 }, + { url = "https://files.pythonhosted.org/packages/bf/5e/ffee22bf9f9e4b2669d1f0179ae8804584939fb6502b51f2401e26b1e028/ruff-0.8.5-py3-none-win_arm64.whl", hash = "sha256:134ae019ef13e1b060ab7136e7828a6d83ea727ba123381307eb37c6bd5e01cb", size = 9124741 }, ] [[package]] From d06b393cdbfc8c18109dceb0eec358b44f415900 Mon Sep 17 00:00:00 2001 From: David Soria Parra Date: Fri, 3 Jan 2025 15:45:57 +0000 Subject: [PATCH 21/24] build: bump version to v1.1.3 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 1e46ae0..45c691e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "mcp" -version = "1.1.2" +version = "1.1.3" description = "Model Context Protocol SDK" readme = "README.md" requires-python = ">=3.10" From 329f595f5f35731a22ed939280846160697cd5f2 Mon Sep 17 00:00:00 2001 From: David Soria Parra Date: Fri, 3 Jan 2025 15:53:19 +0000 Subject: [PATCH 22/24] fix: Fix line length in test_server.py --- tests/server/fastmcp/test_server.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/server/fastmcp/test_server.py b/tests/server/fastmcp/test_server.py index 63841af..f5d4214 100644 --- a/tests/server/fastmcp/test_server.py +++ b/tests/server/fastmcp/test_server.py @@ -36,7 +36,9 @@ class TestServer: mcp = FastMCP() @mcp.tool( - description="🌟 This tool uses emojis and UTF-8 characters: á é í ó ú ñ 漢字 🎉" + description=( + "🌟 This tool uses emojis and UTF-8 characters: á é í ó ú ñ 漢字 🎉" + ) ) def hello_world(name: str = "世界") -> str: return f"¡Hola, {name}! 👋" From e21e8c157dd7592d29f5f69ba5892917f7ba0d0c Mon Sep 17 00:00:00 2001 From: David Soria Parra Date: Fri, 3 Jan 2025 15:57:14 +0000 Subject: [PATCH 23/24] style: Fix import sorting and update ruff version --- examples/servers/simple-prompt/mcp_simple_prompt/server.py | 2 +- examples/servers/simple-resource/mcp_simple_resource/server.py | 2 +- examples/servers/simple-tool/mcp_simple_tool/server.py | 2 +- src/mcp/server/fastmcp/server.py | 2 +- tests/issues/test_100_tool_listing.py | 1 + 5 files changed, 5 insertions(+), 4 deletions(-) diff --git a/examples/servers/simple-prompt/mcp_simple_prompt/server.py b/examples/servers/simple-prompt/mcp_simple_prompt/server.py index 89b0eea..0552f27 100644 --- a/examples/servers/simple-prompt/mcp_simple_prompt/server.py +++ b/examples/servers/simple-prompt/mcp_simple_prompt/server.py @@ -90,7 +90,7 @@ def main(port: int, transport: str) -> int: if transport == "sse": from mcp.server.sse import SseServerTransport from starlette.applications import Starlette - from starlette.routing import Route, Mount + from starlette.routing import Mount, Route sse = SseServerTransport("/messages/") diff --git a/examples/servers/simple-resource/mcp_simple_resource/server.py b/examples/servers/simple-resource/mcp_simple_resource/server.py index 51f2933..11ba569 100644 --- a/examples/servers/simple-resource/mcp_simple_resource/server.py +++ b/examples/servers/simple-resource/mcp_simple_resource/server.py @@ -47,7 +47,7 @@ def main(port: int, transport: str) -> int: if transport == "sse": from mcp.server.sse import SseServerTransport from starlette.applications import Starlette - from starlette.routing import Route, Mount + from starlette.routing import Mount, Route sse = SseServerTransport("/messages/") diff --git a/examples/servers/simple-tool/mcp_simple_tool/server.py b/examples/servers/simple-tool/mcp_simple_tool/server.py index 039af9e..3eace52 100644 --- a/examples/servers/simple-tool/mcp_simple_tool/server.py +++ b/examples/servers/simple-tool/mcp_simple_tool/server.py @@ -60,7 +60,7 @@ def main(port: int, transport: str) -> int: if transport == "sse": from mcp.server.sse import SseServerTransport from starlette.applications import Starlette - from starlette.routing import Route, Mount + from starlette.routing import Mount, Route sse = SseServerTransport("/messages/") diff --git a/src/mcp/server/fastmcp/server.py b/src/mcp/server/fastmcp/server.py index 5b65707..7d3d2ba 100644 --- a/src/mcp/server/fastmcp/server.py +++ b/src/mcp/server/fastmcp/server.py @@ -423,7 +423,7 @@ class FastMCP: async def run_sse_async(self) -> None: """Run the server using SSE transport.""" from starlette.applications import Starlette - from starlette.routing import Route, Mount + from starlette.routing import Mount, Route sse = SseServerTransport("/messages/") diff --git a/tests/issues/test_100_tool_listing.py b/tests/issues/test_100_tool_listing.py index 4bc6d63..2bc386c 100644 --- a/tests/issues/test_100_tool_listing.py +++ b/tests/issues/test_100_tool_listing.py @@ -1,4 +1,5 @@ import pytest + from mcp.server.fastmcp import FastMCP pytestmark = pytest.mark.anyio From e32bf07433f60702dfb3269bb3a40bddaa55c51f Mon Sep 17 00:00:00 2001 From: David Soria Parra Date: Fri, 3 Jan 2025 15:57:56 +0000 Subject: [PATCH 24/24] docs: Update CLAUDE.md with ruff and pre-commit info --- CLAUDE.md | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/CLAUDE.md b/CLAUDE.md index c501926..ef75f97 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -29,10 +29,15 @@ This file is intended to be used by an LLM such as Claude. - Handles both formatting and linting - For formatting: `uv run ruff format .` - For checking: `uv run ruff check .` +- For auto-fixing: `uv run ruff check . --fix` - Common issues: - Line length (default 88 chars) - - Import sorting + - Import sorting (I001 errors) - Unused imports +- When line length errors occur: + - For strings, use parentheses and line continuation + - For function calls, use multiple lines with proper indentation + - For imports, split into multiple lines ### Pyright - Type checker @@ -42,6 +47,18 @@ This file is intended to be used by an LLM such as Claude. - Optional types need explicit None checks - String operations need type narrowing +## Pre-commit Hooks + +- Configuration in `.pre-commit-config.yaml` +- Runs automatically on git commit +- Includes: + - Prettier for YAML/JSON formatting + - Ruff for Python formatting and linting +- When updating ruff version: + - Check available versions on PyPI + - Update `rev` in config to match available version + - Add and commit config changes before other changes + ## Best Practices 1. Always check git status and diff before committing