From a79f51f55fdd6f6a3ea7fb6a32826aa40d00de99 Mon Sep 17 00:00:00 2001 From: David Soria Parra Date: Thu, 19 Dec 2024 22:33:40 +0000 Subject: [PATCH] style: Fix imports and line length formatting --- examples/fastmcp/complex_inputs.py | 4 +- examples/fastmcp/memory.py | 9 ++- examples/fastmcp/readme-quickstart.py | 1 - examples/fastmcp/screenshot.py | 2 +- examples/fastmcp/simple_echo.py | 1 - examples/fastmcp/text_me.py | 1 + .../mcp_simple_resource/server.py | 2 +- pyproject.toml | 1 - src/mcp/cli/__init__.py | 1 - src/mcp/cli/claude.py | 4 +- src/mcp/cli/cli.py | 12 ++-- src/mcp/server/__init__.py | 2 +- src/mcp/server/fastmcp/__init__.py | 3 +- src/mcp/server/fastmcp/prompts/base.py | 9 +-- src/mcp/server/fastmcp/resources/__init__.py | 18 +++--- .../fastmcp/resources/resource_manager.py | 1 - src/mcp/server/fastmcp/resources/types.py | 6 +- src/mcp/server/fastmcp/server.py | 31 ++++----- src/mcp/server/fastmcp/tools/base.py | 15 +++-- src/mcp/server/fastmcp/tools/tool_manager.py | 7 +- .../server/fastmcp/utilities/func_metadata.py | 28 ++++---- src/mcp/server/fastmcp/utilities/logging.py | 2 + src/mcp/server/lowlevel/__init__.py | 2 +- tests/server/fastmcp/prompts/test_base.py | 20 ++++-- tests/server/fastmcp/prompts/test_manager.py | 7 +- .../fastmcp/resources/test_file_resources.py | 8 ++- .../resources/test_function_resources.py | 9 ++- .../resources/test_resource_manager.py | 6 +- .../resources/test_resource_template.py | 7 ++ .../fastmcp/resources/test_resources.py | 1 + .../fastmcp/servers/test_file_server.py | 11 +++- tests/server/fastmcp/test_func_metadata.py | 13 ++-- tests/server/fastmcp/test_server.py | 64 +++++++++++++++---- tests/server/fastmcp/test_tool_manager.py | 20 +++++- tests/test_examples.py | 30 +++++---- tests/test_types.py | 5 +- uv.lock | 14 ---- 37 files changed, 242 insertions(+), 135 deletions(-) diff --git a/examples/fastmcp/complex_inputs.py b/examples/fastmcp/complex_inputs.py index 48aacbe..e859165 100644 --- a/examples/fastmcp/complex_inputs.py +++ b/examples/fastmcp/complex_inputs.py @@ -4,8 +4,10 @@ FastMCP Complex inputs Example Demonstrates validation via pydantic with complex models. """ -from pydantic import BaseModel, Field from typing import Annotated + +from pydantic import BaseModel, Field + from mcp.server.fastmcp import FastMCP mcp = FastMCP("Shrimp Tank") diff --git a/examples/fastmcp/memory.py b/examples/fastmcp/memory.py index 6b114aa..dbc8908 100644 --- a/examples/fastmcp/memory.py +++ b/examples/fastmcp/memory.py @@ -6,7 +6,8 @@ """ Recursive memory system inspired by the human brain's clustering of memories. -Uses OpenAI's 'text-embedding-3-small' model and pgvector for efficient similarity search. +Uses OpenAI's 'text-embedding-3-small' model and pgvector for efficient +similarity search. """ import asyncio @@ -111,7 +112,8 @@ class MemoryNode(BaseModel): if self.id is None: result = await conn.fetchrow( """ - INSERT INTO memories (content, summary, importance, access_count, timestamp, embedding) + INSERT INTO memories (content, summary, importance, access_count, + timestamp, embedding) VALUES ($1, $2, $3, $4, $5, $6) RETURNING id """, @@ -336,7 +338,8 @@ async def initialize_database(): timestamp DOUBLE PRECISION NOT NULL, embedding vector(1536) NOT NULL ); - CREATE INDEX IF NOT EXISTS idx_memories_embedding ON memories USING hnsw (embedding vector_l2_ops); + CREATE INDEX IF NOT EXISTS idx_memories_embedding ON memories + USING hnsw (embedding vector_l2_ops); """) finally: await pool.close() diff --git a/examples/fastmcp/readme-quickstart.py b/examples/fastmcp/readme-quickstart.py index 6c8e275..d1c522a 100644 --- a/examples/fastmcp/readme-quickstart.py +++ b/examples/fastmcp/readme-quickstart.py @@ -1,6 +1,5 @@ from mcp.server.fastmcp import FastMCP - # Create an MCP server mcp = FastMCP("Demo") diff --git a/examples/fastmcp/screenshot.py b/examples/fastmcp/screenshot.py index 34c9a61..694b49f 100644 --- a/examples/fastmcp/screenshot.py +++ b/examples/fastmcp/screenshot.py @@ -5,10 +5,10 @@ Give Claude a tool to capture and view screenshots. """ import io + from mcp.server.fastmcp import FastMCP from mcp.server.fastmcp.utilities.types import Image - # Create server mcp = FastMCP("Screenshot Demo", dependencies=["pyautogui", "Pillow"]) diff --git a/examples/fastmcp/simple_echo.py b/examples/fastmcp/simple_echo.py index a9aa7d2..c261526 100644 --- a/examples/fastmcp/simple_echo.py +++ b/examples/fastmcp/simple_echo.py @@ -4,7 +4,6 @@ FastMCP Echo Server from mcp.server.fastmcp import FastMCP - # Create server mcp = FastMCP("Echo Server") diff --git a/examples/fastmcp/text_me.py b/examples/fastmcp/text_me.py index 803f024..8053c6c 100644 --- a/examples/fastmcp/text_me.py +++ b/examples/fastmcp/text_me.py @@ -19,6 +19,7 @@ Visit https://surgemsg.com/ and click "Get Started" to obtain these values. """ from typing import Annotated + import httpx from pydantic import BeforeValidator from pydantic_settings import BaseSettings, SettingsConfigDict diff --git a/examples/servers/simple-resource/mcp_simple_resource/server.py b/examples/servers/simple-resource/mcp_simple_resource/server.py index 6009c7b..9864fc5 100644 --- a/examples/servers/simple-resource/mcp_simple_resource/server.py +++ b/examples/servers/simple-resource/mcp_simple_resource/server.py @@ -1,8 +1,8 @@ import anyio import click import mcp.types as types -from pydantic import AnyUrl from mcp.server.lowlevel import Server +from pydantic import AnyUrl SAMPLE_RESOURCES = { "greeting": "Hello! This is a sample text resource.", diff --git a/pyproject.toml b/pyproject.toml index 4d532fd..f64f3f4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,7 +47,6 @@ dev-dependencies = [ "trio>=0.26.2", "pytest-flakefinder>=1.1.0", "pytest-xdist>=3.6.1", - "pytest-asyncio>=0.24.0", ] [build-system] diff --git a/src/mcp/cli/__init__.py b/src/mcp/cli/__init__.py index 4de8058..3ef56d8 100644 --- a/src/mcp/cli/__init__.py +++ b/src/mcp/cli/__init__.py @@ -2,6 +2,5 @@ from .cli import app - if __name__ == "__main__": app() diff --git a/src/mcp/cli/claude.py b/src/mcp/cli/claude.py index 7182d4a..9e2ef6c 100644 --- a/src/mcp/cli/claude.py +++ b/src/mcp/cli/claude.py @@ -48,8 +48,8 @@ def update_claude_config( config_dir = get_claude_config_path() if not config_dir: raise RuntimeError( - "Claude Desktop config directory not found. Please ensure Claude Desktop " - "is installed and has been run at least once to initialize its configuration." + "Claude Desktop config directory not found. Please ensure Claude Desktop" + " is installed and has been run at least once to initialize its config." ) config_file = config_dir / "claude_desktop_config.json" diff --git a/src/mcp/cli/cli.py b/src/mcp/cli/cli.py index c1728ae..3e164d9 100644 --- a/src/mcp/cli/cli.py +++ b/src/mcp/cli/cli.py @@ -295,7 +295,8 @@ def run( """Run a MCP server. The server can be specified in two ways: - 1. Module approach: server.py - runs the module directly, expecting a server.run() call + 1. Module approach: server.py - runs the module directly, expecting a server.run() + call 2. Import approach: server.py:app - imports and runs the specified server object Note: This command runs the server directly. You are responsible for ensuring @@ -346,7 +347,8 @@ def install( typer.Option( "--name", "-n", - help="Custom name for the server (defaults to server's name attribute or file name)", + help="Custom name for the server (defaults to server's name attribute or" + " file name)", ), ] = None, with_editable: Annotated[ @@ -410,7 +412,8 @@ def install( logger.error("Claude app not found") sys.exit(1) - # Try to import server to get its name, but fall back to file name if dependencies missing + # Try to import server to get its name, but fall back to file name if dependencies + # missing name = server_name server = None if not name: @@ -419,7 +422,8 @@ def install( name = server.name except (ImportError, ModuleNotFoundError) as e: logger.debug( - "Could not import server (likely missing dependencies), using file name", + "Could not import server (likely missing dependencies), using file" + " name", extra={"error": str(e)}, ) name = file.stem diff --git a/src/mcp/server/__init__.py b/src/mcp/server/__init__.py index 4db3e6d..8ffbe1e 100644 --- a/src/mcp/server/__init__.py +++ b/src/mcp/server/__init__.py @@ -1,4 +1,4 @@ -from .lowlevel import Server, NotificationOptions from .fastmcp import FastMCP +from .lowlevel import NotificationOptions, Server __all__ = ["Server", "FastMCP", "NotificationOptions"] diff --git a/src/mcp/server/fastmcp/__init__.py b/src/mcp/server/fastmcp/__init__.py index 4ff1a05..84b0520 100644 --- a/src/mcp/server/fastmcp/__init__.py +++ b/src/mcp/server/fastmcp/__init__.py @@ -1,7 +1,8 @@ """FastMCP - A more ergonomic interface for MCP servers.""" from importlib.metadata import version -from .server import FastMCP, Context + +from .server import Context, FastMCP from .utilities.types import Image __version__ = version("mcp") diff --git a/src/mcp/server/fastmcp/prompts/base.py b/src/mcp/server/fastmcp/prompts/base.py index 8358f4b..0df3d2f 100644 --- a/src/mcp/server/fastmcp/prompts/base.py +++ b/src/mcp/server/fastmcp/prompts/base.py @@ -1,13 +1,14 @@ """Base classes for FastMCP prompts.""" -import json -from typing import Any, Literal, Sequence, Awaitable import inspect +import json from collections.abc import Callable +from typing import Any, Awaitable, Literal, Sequence -from pydantic import BaseModel, Field, TypeAdapter, validate_call -from mcp.types import TextContent, ImageContent, EmbeddedResource import pydantic_core +from pydantic import BaseModel, Field, TypeAdapter, validate_call + +from mcp.types import EmbeddedResource, ImageContent, TextContent CONTENT_TYPES = TextContent | ImageContent | EmbeddedResource diff --git a/src/mcp/server/fastmcp/resources/__init__.py b/src/mcp/server/fastmcp/resources/__init__.py index 92deb87..b5805fb 100644 --- a/src/mcp/server/fastmcp/resources/__init__.py +++ b/src/mcp/server/fastmcp/resources/__init__.py @@ -1,14 +1,14 @@ from .base import Resource -from .types import ( - TextResource, - BinaryResource, - FunctionResource, - FileResource, - HttpResource, - DirectoryResource, -) -from .templates import ResourceTemplate from .resource_manager import ResourceManager +from .templates import ResourceTemplate +from .types import ( + BinaryResource, + DirectoryResource, + FileResource, + FunctionResource, + HttpResource, + TextResource, +) __all__ = [ "Resource", diff --git a/src/mcp/server/fastmcp/resources/resource_manager.py b/src/mcp/server/fastmcp/resources/resource_manager.py index 1f9561e..ded34bf 100644 --- a/src/mcp/server/fastmcp/resources/resource_manager.py +++ b/src/mcp/server/fastmcp/resources/resource_manager.py @@ -1,7 +1,6 @@ """Resource manager functionality.""" from typing import Callable -from collections.abc import Iterable from pydantic import AnyUrl diff --git a/src/mcp/server/fastmcp/resources/types.py b/src/mcp/server/fastmcp/resources/types.py index b1a8088..97de7c2 100644 --- a/src/mcp/server/fastmcp/resources/types.py +++ b/src/mcp/server/fastmcp/resources/types.py @@ -1,11 +1,11 @@ """Concrete resource implementations.""" -import anyio import json -from pathlib import Path -from typing import Any, Callable from collections.abc import Callable +from pathlib import Path +from typing import Any +import anyio import httpx import pydantic.json import pydantic_core diff --git a/src/mcp/server/fastmcp/server.py b/src/mcp/server/fastmcp/server.py index b00627e..f090928 100644 --- a/src/mcp/server/fastmcp/server.py +++ b/src/mcp/server/fastmcp/server.py @@ -1,17 +1,25 @@ """FastMCP - A more ergonomic interface for MCP servers.""" -import anyio import functools import inspect import json import re from itertools import chain from typing import Any, Callable, Literal, Sequence -from collections.abc import Iterable +import anyio import pydantic_core -from pydantic import Field import uvicorn +from pydantic import BaseModel, Field +from pydantic.networks import AnyUrl +from pydantic_settings import BaseSettings, SettingsConfigDict + +from mcp.server.fastmcp.exceptions import ResourceError +from mcp.server.fastmcp.prompts import Prompt, PromptManager +from mcp.server.fastmcp.resources import FunctionResource, Resource, ResourceManager +from mcp.server.fastmcp.tools import ToolManager +from mcp.server.fastmcp.utilities.logging import configure_logging, get_logger +from mcp.server.fastmcp.utilities.types import Image from mcp.server.lowlevel import Server as MCPServer from mcp.server.sse import SseServerTransport from mcp.server.stdio import stdio_server @@ -24,6 +32,8 @@ from mcp.types import ( ) from mcp.types import ( Prompt as MCPPrompt, +) +from mcp.types import ( PromptArgument as MCPPromptArgument, ) from mcp.types import ( @@ -35,16 +45,6 @@ from mcp.types import ( from mcp.types import ( Tool as MCPTool, ) -from pydantic import BaseModel -from pydantic.networks import AnyUrl -from pydantic_settings import BaseSettings, SettingsConfigDict - -from mcp.server.fastmcp.exceptions import ResourceError -from mcp.server.fastmcp.prompts import Prompt, PromptManager -from mcp.server.fastmcp.resources import FunctionResource, Resource, ResourceManager -from mcp.server.fastmcp.tools import ToolManager -from mcp.server.fastmcp.utilities.logging import configure_logging, get_logger -from mcp.server.fastmcp.utilities.types import Image logger = get_logger(__name__) @@ -226,8 +226,9 @@ class FastMCP: def tool(self, name: str | None = None, description: str | None = None) -> Callable: """Decorator to register a tool. - Tools can optionally request a Context object by adding a parameter with the Context type annotation. - The context provides access to MCP capabilities like logging, progress reporting, and resource access. + Tools can optionally request a Context object by adding a parameter with the + Context type annotation. The context provides access to MCP capabilities like + logging, progress reporting, and resource access. Args: name: Optional name for the tool (defaults to function name) diff --git a/src/mcp/server/fastmcp/tools/base.py b/src/mcp/server/fastmcp/tools/base.py index 8f2ea48..a8751a5 100644 --- a/src/mcp/server/fastmcp/tools/base.py +++ b/src/mcp/server/fastmcp/tools/base.py @@ -1,12 +1,12 @@ -import mcp.server.fastmcp -from mcp.server.fastmcp.exceptions import ToolError -from mcp.server.fastmcp.utilities.func_metadata import func_metadata, FuncMetadata -from pydantic import BaseModel, Field - - import inspect from typing import TYPE_CHECKING, Any, Callable +from pydantic import BaseModel, Field + +import mcp.server.fastmcp +from mcp.server.fastmcp.exceptions import ToolError +from mcp.server.fastmcp.utilities.func_metadata import FuncMetadata, func_metadata + if TYPE_CHECKING: from mcp.server.fastmcp.server import Context @@ -19,7 +19,8 @@ class Tool(BaseModel): description: str = Field(description="Description of what the tool does") parameters: dict = Field(description="JSON schema for tool parameters") fn_metadata: FuncMetadata = Field( - description="Metadata about the function including a pydantic model for tool arguments" + description="Metadata about the function including a pydantic model for tool" + " arguments" ) is_async: bool = Field(description="Whether the tool is async") context_kwarg: str | None = Field( diff --git a/src/mcp/server/fastmcp/tools/tool_manager.py b/src/mcp/server/fastmcp/tools/tool_manager.py index 52b45d1..807c26b 100644 --- a/src/mcp/server/fastmcp/tools/tool_manager.py +++ b/src/mcp/server/fastmcp/tools/tool_manager.py @@ -1,9 +1,8 @@ +from collections.abc import Callable +from typing import TYPE_CHECKING, Any + from mcp.server.fastmcp.exceptions import ToolError from mcp.server.fastmcp.tools.base import Tool - -from typing import Any, Callable, TYPE_CHECKING -from collections.abc import Callable - from mcp.server.fastmcp.utilities.logging import get_logger if TYPE_CHECKING: diff --git a/src/mcp/server/fastmcp/utilities/func_metadata.py b/src/mcp/server/fastmcp/utilities/func_metadata.py index b1f1385..cf93049 100644 --- a/src/mcp/server/fastmcp/utilities/func_metadata.py +++ b/src/mcp/server/fastmcp/utilities/func_metadata.py @@ -1,21 +1,19 @@ import inspect -from collections.abc import Callable, Sequence, Awaitable +import json +from collections.abc import Awaitable, Callable, Sequence from typing import ( Annotated, Any, ForwardRef, ) -from pydantic import Field -from mcp.server.fastmcp.exceptions import InvalidSignature -from pydantic._internal._typing_extra import eval_type_backport -import json -from pydantic import BaseModel -from pydantic.fields import FieldInfo -from pydantic import ConfigDict, create_model -from pydantic import WithJsonSchema -from pydantic_core import PydanticUndefined -from mcp.server.fastmcp.utilities.logging import get_logger +from pydantic import BaseModel, ConfigDict, Field, WithJsonSchema, create_model +from pydantic._internal._typing_extra import eval_type_backport +from pydantic.fields import FieldInfo +from pydantic_core import PydanticUndefined + +from mcp.server.fastmcp.exceptions import InvalidSignature +from mcp.server.fastmcp.utilities.logging import get_logger logger = get_logger(__name__) @@ -105,7 +103,8 @@ class FuncMetadata(BaseModel): def func_metadata(func: Callable, skip_names: Sequence[str] = ()) -> FuncMetadata: - """Given a function, return metadata including a pydantic model representing its signature. + """Given a function, return metadata including a pydantic model representing its + signature. The use case for this is ``` @@ -114,7 +113,8 @@ def func_metadata(func: Callable, skip_names: Sequence[str] = ()) -> FuncMetadat return func(**validated_args.model_dump_one_level()) ``` - **critically** it also provides pre-parse helper to attempt to parse things from JSON. + **critically** it also provides pre-parse helper to attempt to parse things from + JSON. Args: func: The function to convert to a pydantic model @@ -130,7 +130,7 @@ def func_metadata(func: Callable, skip_names: Sequence[str] = ()) -> FuncMetadat for param in params.values(): if param.name.startswith("_"): raise InvalidSignature( - f"Parameter {param.name} of {func.__name__} may not start with an underscore" + f"Parameter {param.name} of {func.__name__} cannot start with '_'" ) if param.name in skip_names: continue diff --git a/src/mcp/server/fastmcp/utilities/logging.py b/src/mcp/server/fastmcp/utilities/logging.py index 60738f8..df9da43 100644 --- a/src/mcp/server/fastmcp/utilities/logging.py +++ b/src/mcp/server/fastmcp/utilities/logging.py @@ -3,6 +3,7 @@ import logging from typing import Literal + def get_logger(name: str) -> logging.Logger: """Get a logger nested under MCPnamespace. @@ -27,6 +28,7 @@ def configure_logging( try: from rich.console import Console from rich.logging import RichHandler + handlers.append(RichHandler(console=Console(stderr=True), rich_tracebacks=True)) except ImportError: pass diff --git a/src/mcp/server/lowlevel/__init__.py b/src/mcp/server/lowlevel/__init__.py index a6dff43..66df389 100644 --- a/src/mcp/server/lowlevel/__init__.py +++ b/src/mcp/server/lowlevel/__init__.py @@ -1,3 +1,3 @@ -from .server import Server, NotificationOptions +from .server import NotificationOptions, Server __all__ = ["Server", "NotificationOptions"] diff --git a/tests/server/fastmcp/prompts/test_base.py b/tests/server/fastmcp/prompts/test_base.py index 63dc230..bb47d6d 100644 --- a/tests/server/fastmcp/prompts/test_base.py +++ b/tests/server/fastmcp/prompts/test_base.py @@ -1,16 +1,18 @@ -from pydantic import FileUrl import pytest +from pydantic import FileUrl + from mcp.server.fastmcp.prompts.base import ( - Prompt, - UserMessage, - TextContent, AssistantMessage, Message, + Prompt, + TextContent, + UserMessage, ) from mcp.types import EmbeddedResource, TextResourceContents class TestRenderPrompt: + @pytest.mark.anyio async def test_basic_fn(self): def fn() -> str: return "Hello, world!" @@ -20,6 +22,7 @@ class TestRenderPrompt: UserMessage(content=TextContent(type="text", text="Hello, world!")) ] + @pytest.mark.anyio async def test_async_fn(self): async def fn() -> str: return "Hello, world!" @@ -29,6 +32,7 @@ class TestRenderPrompt: UserMessage(content=TextContent(type="text", text="Hello, world!")) ] + @pytest.mark.anyio async def test_fn_with_args(self): async def fn(name: str, age: int = 30) -> str: return f"Hello, {name}! You're {age} years old." @@ -42,6 +46,7 @@ class TestRenderPrompt: ) ] + @pytest.mark.anyio async def test_fn_with_invalid_kwargs(self): async def fn(name: str, age: int = 30) -> str: return f"Hello, {name}! You're {age} years old." @@ -50,6 +55,7 @@ class TestRenderPrompt: with pytest.raises(ValueError): await prompt.render(arguments=dict(age=40)) + @pytest.mark.anyio async def test_fn_returns_message(self): async def fn() -> UserMessage: return UserMessage(content="Hello, world!") @@ -59,6 +65,7 @@ class TestRenderPrompt: UserMessage(content=TextContent(type="text", text="Hello, world!")) ] + @pytest.mark.anyio async def test_fn_returns_assistant_message(self): async def fn() -> AssistantMessage: return AssistantMessage( @@ -70,6 +77,7 @@ class TestRenderPrompt: AssistantMessage(content=TextContent(type="text", text="Hello, world!")) ] + @pytest.mark.anyio async def test_fn_returns_multiple_messages(self): expected = [ UserMessage("Hello, world!"), @@ -83,6 +91,7 @@ class TestRenderPrompt: prompt = Prompt.from_function(fn) assert await prompt.render() == expected + @pytest.mark.anyio async def test_fn_returns_list_of_strings(self): expected = [ "Hello, world!", @@ -95,6 +104,7 @@ class TestRenderPrompt: prompt = Prompt.from_function(fn) assert await prompt.render() == [UserMessage(t) for t in expected] + @pytest.mark.anyio async def test_fn_returns_resource_content(self): """Test returning a message with resource content.""" @@ -124,6 +134,7 @@ class TestRenderPrompt: ) ] + @pytest.mark.anyio async def test_fn_returns_mixed_content(self): """Test returning messages with mixed content types.""" @@ -163,6 +174,7 @@ class TestRenderPrompt: ), ] + @pytest.mark.anyio async def test_fn_returns_dict_with_resource(self): """Test returning a dict with resource content.""" diff --git a/tests/server/fastmcp/prompts/test_manager.py b/tests/server/fastmcp/prompts/test_manager.py index 7b97b30..c64a4a5 100644 --- a/tests/server/fastmcp/prompts/test_manager.py +++ b/tests/server/fastmcp/prompts/test_manager.py @@ -1,5 +1,6 @@ import pytest -from mcp.server.fastmcp.prompts.base import UserMessage, TextContent, Prompt + +from mcp.server.fastmcp.prompts.base import Prompt, TextContent, UserMessage from mcp.server.fastmcp.prompts.manager import PromptManager @@ -60,6 +61,7 @@ class TestPromptManager: assert len(prompts) == 2 assert prompts == [prompt1, prompt2] + @pytest.mark.anyio async def test_render_prompt(self): """Test rendering a prompt.""" @@ -74,6 +76,7 @@ class TestPromptManager: UserMessage(content=TextContent(type="text", text="Hello, world!")) ] + @pytest.mark.anyio async def test_render_prompt_with_args(self): """Test rendering a prompt with arguments.""" @@ -88,12 +91,14 @@ class TestPromptManager: UserMessage(content=TextContent(type="text", text="Hello, World!")) ] + @pytest.mark.anyio async def test_render_unknown_prompt(self): """Test rendering a non-existent prompt.""" manager = PromptManager() with pytest.raises(ValueError, match="Unknown prompt: unknown"): await manager.render_prompt("unknown") + @pytest.mark.anyio async def test_render_prompt_with_missing_args(self): """Test rendering a prompt with missing required arguments.""" diff --git a/tests/server/fastmcp/resources/test_file_resources.py b/tests/server/fastmcp/resources/test_file_resources.py index f9ec159..36cbca3 100644 --- a/tests/server/fastmcp/resources/test_file_resources.py +++ b/tests/server/fastmcp/resources/test_file_resources.py @@ -1,8 +1,8 @@ import os - -import pytest from pathlib import Path from tempfile import NamedTemporaryFile + +import pytest from pydantic import FileUrl from mcp.server.fastmcp.resources import FileResource @@ -53,6 +53,7 @@ class TestFileResource: assert isinstance(resource.path, Path) assert resource.path.is_absolute() + @pytest.mark.anyio async def test_read_text_file(self, temp_file: Path): """Test reading a text file.""" resource = FileResource( @@ -64,6 +65,7 @@ class TestFileResource: assert content == "test content" assert resource.mime_type == "text/plain" + @pytest.mark.anyio async def test_read_binary_file(self, temp_file: Path): """Test reading a file as binary.""" resource = FileResource( @@ -85,6 +87,7 @@ class TestFileResource: path=Path("test.txt"), ) + @pytest.mark.anyio async def test_missing_file_error(self, temp_file: Path): """Test error when file doesn't exist.""" # Create path to non-existent file @@ -100,6 +103,7 @@ class TestFileResource: @pytest.mark.skipif( os.name == "nt", reason="File permissions behave differently on Windows" ) + @pytest.mark.anyio async def test_permission_error(self, temp_file: Path): """Test reading a file without permissions.""" temp_file.chmod(0o000) # Remove all permissions diff --git a/tests/server/fastmcp/resources/test_function_resources.py b/tests/server/fastmcp/resources/test_function_resources.py index e132e5f..b92af5c 100644 --- a/tests/server/fastmcp/resources/test_function_resources.py +++ b/tests/server/fastmcp/resources/test_function_resources.py @@ -1,5 +1,6 @@ -from pydantic import BaseModel, AnyUrl import pytest +from pydantic import AnyUrl, BaseModel + from mcp.server.fastmcp.resources import FunctionResource @@ -24,6 +25,7 @@ class TestFunctionResource: assert resource.mime_type == "text/plain" # default assert resource.fn == my_func + @pytest.mark.anyio async def test_read_text(self): """Test reading text from a FunctionResource.""" @@ -39,6 +41,7 @@ class TestFunctionResource: assert content == "Hello, world!" assert resource.mime_type == "text/plain" + @pytest.mark.anyio async def test_read_binary(self): """Test reading binary data from a FunctionResource.""" @@ -53,6 +56,7 @@ class TestFunctionResource: content = await resource.read() assert content == b"Hello, world!" + @pytest.mark.anyio async def test_json_conversion(self): """Test automatic JSON conversion of non-string results.""" @@ -68,6 +72,7 @@ class TestFunctionResource: assert isinstance(content, str) assert '"key": "value"' in content + @pytest.mark.anyio async def test_error_handling(self): """Test error handling in FunctionResource.""" @@ -82,6 +87,7 @@ class TestFunctionResource: with pytest.raises(ValueError, match="Error reading resource function://test"): await resource.read() + @pytest.mark.anyio async def test_basemodel_conversion(self): """Test handling of BaseModel types.""" @@ -96,6 +102,7 @@ class TestFunctionResource: content = await resource.read() assert content == '{"name": "test"}' + @pytest.mark.anyio async def test_custom_type_conversion(self): """Test handling of custom types.""" diff --git a/tests/server/fastmcp/resources/test_resource_manager.py b/tests/server/fastmcp/resources/test_resource_manager.py index d8d04e5..4423e53 100644 --- a/tests/server/fastmcp/resources/test_resource_manager.py +++ b/tests/server/fastmcp/resources/test_resource_manager.py @@ -1,6 +1,7 @@ -import pytest from pathlib import Path from tempfile import NamedTemporaryFile + +import pytest from pydantic import AnyUrl, FileUrl from mcp.server.fastmcp.resources import ( @@ -80,6 +81,7 @@ class TestResourceManager: manager.add_resource(resource) assert "Resource already exists" not in caplog.text + @pytest.mark.anyio async def test_get_resource(self, temp_file: Path): """Test getting a resource by URI.""" manager = ResourceManager() @@ -92,6 +94,7 @@ class TestResourceManager: retrieved = await manager.get_resource(resource.uri) assert retrieved == resource + @pytest.mark.anyio async def test_get_resource_from_template(self): """Test getting a resource through a template.""" manager = ResourceManager() @@ -111,6 +114,7 @@ class TestResourceManager: content = await resource.read() assert content == "Hello, world!" + @pytest.mark.anyio async def test_get_unknown_resource(self): """Test getting a non-existent resource.""" manager = ResourceManager() diff --git a/tests/server/fastmcp/resources/test_resource_template.py b/tests/server/fastmcp/resources/test_resource_template.py index 95d0585..09bc600 100644 --- a/tests/server/fastmcp/resources/test_resource_template.py +++ b/tests/server/fastmcp/resources/test_resource_template.py @@ -1,4 +1,5 @@ import json + import pytest from pydantic import BaseModel @@ -45,6 +46,7 @@ class TestResourceTemplate: assert template.matches("test://foo") is None assert template.matches("other://foo/123") is None + @pytest.mark.anyio async def test_create_resource(self): """Test creating a resource from a template.""" @@ -68,6 +70,7 @@ class TestResourceTemplate: data = json.loads(content) assert data == {"key": "foo", "value": 123} + @pytest.mark.anyio async def test_template_error(self): """Test error handling in template resource creation.""" @@ -83,6 +86,7 @@ class TestResourceTemplate: with pytest.raises(ValueError, match="Error creating resource from template"): await template.create_resource("fail://test", {"x": "test"}) + @pytest.mark.anyio async def test_async_text_resource(self): """Test creating a text resource from async function.""" @@ -104,6 +108,7 @@ class TestResourceTemplate: content = await resource.read() assert content == "Hello, world!" + @pytest.mark.anyio async def test_async_binary_resource(self): """Test creating a binary resource from async function.""" @@ -125,6 +130,7 @@ class TestResourceTemplate: content = await resource.read() assert content == b"test" + @pytest.mark.anyio async def test_basemodel_conversion(self): """Test handling of BaseModel types.""" @@ -152,6 +158,7 @@ class TestResourceTemplate: data = json.loads(content) assert data == {"key": "foo", "value": 123} + @pytest.mark.anyio async def test_custom_type_conversion(self): """Test handling of custom types.""" diff --git a/tests/server/fastmcp/resources/test_resources.py b/tests/server/fastmcp/resources/test_resources.py index dddcd56..08b3e65 100644 --- a/tests/server/fastmcp/resources/test_resources.py +++ b/tests/server/fastmcp/resources/test_resources.py @@ -90,6 +90,7 @@ class TestResourceValidation: ) assert resource.mime_type == "application/json" + @pytest.mark.anyio async def test_resource_read_abstract(self): """Test that Resource.read() is abstract.""" diff --git a/tests/server/fastmcp/servers/test_file_server.py b/tests/server/fastmcp/servers/test_file_server.py index d9b34d6..28773b1 100644 --- a/tests/server/fastmcp/servers/test_file_server.py +++ b/tests/server/fastmcp/servers/test_file_server.py @@ -1,8 +1,10 @@ import json -from mcp.server.fastmcp import FastMCP -import pytest from pathlib import Path +import pytest + +from mcp.server.fastmcp import FastMCP + @pytest.fixture() def test_dir(tmp_path_factory) -> Path: @@ -71,6 +73,7 @@ def tools(mcp: FastMCP, test_dir: Path) -> FastMCP: return mcp +@pytest.mark.anyio async def test_list_resources(mcp: FastMCP): resources = await mcp.list_resources() assert len(resources) == 4 @@ -83,6 +86,7 @@ async def test_list_resources(mcp: FastMCP): ] +@pytest.mark.anyio async def test_read_resource_dir(mcp: FastMCP): files = await mcp.read_resource("dir://test_dir") files = json.loads(files) @@ -94,11 +98,13 @@ async def test_read_resource_dir(mcp: FastMCP): ] +@pytest.mark.anyio async def test_read_resource_file(mcp: FastMCP): result = await mcp.read_resource("file://test_dir/example.py") assert result == "print('hello world')" +@pytest.mark.anyio async def test_delete_file(mcp: FastMCP, test_dir: Path): await mcp.call_tool( "delete_file", arguments=dict(path=str(test_dir / "example.py")) @@ -106,6 +112,7 @@ async def test_delete_file(mcp: FastMCP, test_dir: Path): assert not (test_dir / "example.py").exists() +@pytest.mark.anyio async def test_delete_file_and_check_resources(mcp: FastMCP, test_dir: Path): await mcp.call_tool( "delete_file", arguments=dict(path=str(test_dir / "example.py")) diff --git a/tests/server/fastmcp/test_func_metadata.py b/tests/server/fastmcp/test_func_metadata.py index 044798d..8619e47 100644 --- a/tests/server/fastmcp/test_func_metadata.py +++ b/tests/server/fastmcp/test_func_metadata.py @@ -85,6 +85,7 @@ def complex_arguments_fn( return "ok!" +@pytest.mark.anyio async def test_complex_function_runtime_arg_validation_non_json(): """Test that basic non-JSON arguments are validated correctly""" meta = func_metadata(complex_arguments_fn) @@ -121,6 +122,7 @@ async def test_complex_function_runtime_arg_validation_non_json(): ) +@pytest.mark.anyio async def test_complex_function_runtime_arg_validation_with_json(): """Test that JSON string arguments are parsed and validated correctly""" meta = func_metadata(complex_arguments_fn) @@ -140,7 +142,7 @@ async def test_complex_function_runtime_arg_validation_with_json(): "unannotated": "test", "my_model_a": "{}", # JSON string "my_model_a_forward_ref": "{}", # JSON string - "my_model_b": '{"how_many_shrimp": 5, "ok": {"x": 1}, "y": null}', # JSON string + "my_model_b": '{"how_many_shrimp": 5, "ok": {"x": 1}, "y": null}', }, arguments_to_pass_directly=None, ) @@ -197,6 +199,7 @@ def test_skip_names(): assert model.also_keep == 2.5 # type: ignore +@pytest.mark.anyio async def test_lambda_function(): """Test lambda function schema and validation""" fn = lambda x, y=5: x # noqa: E731 @@ -297,7 +300,7 @@ def test_complex_function_json_schema(): }, "field_with_default_via_field_annotation_before_nondefault_arg": { "default": 1, - "title": "Field With Default Via Field Annotation Before Nondefault Arg", + "title": "Field With Default Via Field Annotation Before Arg", "type": "integer", }, "unannotated": {"title": "unannotated", "type": "string"}, @@ -316,11 +319,7 @@ def test_complex_function_json_schema(): "type": "string", }, "my_model_a_with_default": { - "allOf": [ - { - "$ref": "#/$defs/SomeInputModelA" - } - ], + "allOf": [{"$ref": "#/$defs/SomeInputModelA"}], "default": {}, }, "an_int_with_default": { diff --git a/tests/server/fastmcp/test_server.py b/tests/server/fastmcp/test_server.py index 27e13f2..f5aa4cd 100644 --- a/tests/server/fastmcp/test_server.py +++ b/tests/server/fastmcp/test_server.py @@ -3,32 +3,34 @@ from pathlib import Path from typing import TYPE_CHECKING, Union import pytest -from mcp.shared.exceptions import McpError -from mcp.shared.memory import ( - create_connected_server_and_client_session as client_session, -) -from mcp.types import ( - ImageContent, - TextContent, - TextResourceContents, - BlobResourceContents, -) from pydantic import AnyUrl from mcp.server.fastmcp import Context, FastMCP from mcp.server.fastmcp.prompts.base import EmbeddedResource, Message, UserMessage from mcp.server.fastmcp.resources import FileResource, FunctionResource from mcp.server.fastmcp.utilities.types import Image +from mcp.shared.exceptions import McpError +from mcp.shared.memory import ( + create_connected_server_and_client_session as client_session, +) +from mcp.types import ( + BlobResourceContents, + ImageContent, + TextContent, + TextResourceContents, +) if TYPE_CHECKING: from mcp.server.fastmcp import Context class TestServer: + @pytest.mark.anyio async def test_create_server(self): mcp = FastMCP() assert mcp.name == "FastMCP" + @pytest.mark.anyio async def test_add_tool_decorator(self): mcp = FastMCP() @@ -38,6 +40,7 @@ class TestServer: assert len(mcp._tool_manager.list_tools()) == 1 + @pytest.mark.anyio async def test_add_tool_decorator_incorrect_usage(self): mcp = FastMCP() @@ -47,6 +50,7 @@ class TestServer: def add(x: int, y: int) -> int: return x + y + @pytest.mark.anyio async def test_add_resource_decorator(self): mcp = FastMCP() @@ -56,6 +60,7 @@ class TestServer: assert len(mcp._resource_manager._templates) == 1 + @pytest.mark.anyio async def test_add_resource_decorator_incorrect_usage(self): mcp = FastMCP() @@ -88,12 +93,14 @@ def mixed_content_tool_fn() -> list[Union[TextContent, ImageContent]]: class TestServerTools: + @pytest.mark.anyio async def test_add_tool(self): mcp = FastMCP() mcp.add_tool(tool_fn) mcp.add_tool(tool_fn) assert len(mcp._tool_manager.list_tools()) == 1 + @pytest.mark.anyio async def test_list_tools(self): mcp = FastMCP() mcp.add_tool(tool_fn) @@ -101,6 +108,7 @@ class TestServerTools: tools = await client.list_tools() assert len(tools.tools) == 1 + @pytest.mark.anyio async def test_call_tool(self): mcp = FastMCP() mcp.add_tool(tool_fn) @@ -109,6 +117,7 @@ class TestServerTools: assert not hasattr(result, "error") assert len(result.content) > 0 + @pytest.mark.anyio async def test_tool_exception_handling(self): mcp = FastMCP() mcp.add_tool(error_tool_fn) @@ -120,6 +129,7 @@ class TestServerTools: assert "Test error" in content.text assert result.isError is True + @pytest.mark.anyio async def test_tool_error_handling(self): mcp = FastMCP() mcp.add_tool(error_tool_fn) @@ -131,6 +141,7 @@ class TestServerTools: assert "Test error" in content.text assert result.isError is True + @pytest.mark.anyio async def test_tool_error_details(self): """Test that exception details are properly formatted in the response""" mcp = FastMCP() @@ -143,6 +154,7 @@ class TestServerTools: assert "Test error" in content.text assert result.isError is True + @pytest.mark.anyio async def test_tool_return_value_conversion(self): mcp = FastMCP() mcp.add_tool(tool_fn) @@ -153,6 +165,7 @@ class TestServerTools: assert isinstance(content, TextContent) assert content.text == "3" + @pytest.mark.anyio async def test_tool_image_helper(self, tmp_path: Path): # Create a test image image_path = tmp_path / "test.png" @@ -171,6 +184,7 @@ class TestServerTools: decoded = base64.b64decode(content.data) assert decoded == b"fake png data" + @pytest.mark.anyio async def test_tool_mixed_content(self): mcp = FastMCP() mcp.add_tool(mixed_content_tool_fn) @@ -185,8 +199,10 @@ class TestServerTools: assert content2.mimeType == "image/png" assert content2.data == "abc" + @pytest.mark.anyio async def test_tool_mixed_list_with_image(self, tmp_path: Path): - """Test that lists containing Image objects and other types are handled correctly""" + """Test that lists containing Image objects and other types are handled + correctly""" # Create a test image image_path = tmp_path / "test.png" image_path.write_bytes(b"test image data") @@ -224,6 +240,7 @@ class TestServerTools: class TestServerResources: + @pytest.mark.anyio async def test_text_resource(self): mcp = FastMCP() @@ -240,6 +257,7 @@ class TestServerResources: assert isinstance(result.contents[0], TextResourceContents) assert result.contents[0].text == "Hello, world!" + @pytest.mark.anyio async def test_binary_resource(self): mcp = FastMCP() @@ -259,6 +277,7 @@ class TestServerResources: assert isinstance(result.contents[0], BlobResourceContents) assert result.contents[0].blob == base64.b64encode(b"Binary data").decode() + @pytest.mark.anyio async def test_file_resource_text(self, tmp_path: Path): mcp = FastMCP() @@ -276,6 +295,7 @@ class TestServerResources: assert isinstance(result.contents[0], TextResourceContents) assert result.contents[0].text == "Hello from file!" + @pytest.mark.anyio async def test_file_resource_binary(self, tmp_path: Path): mcp = FastMCP() @@ -301,6 +321,7 @@ class TestServerResources: class TestServerResourceTemplates: + @pytest.mark.anyio async def test_resource_with_params(self): """Test that a resource with function parameters raises an error if the URI parameters don't match""" @@ -312,6 +333,7 @@ class TestServerResourceTemplates: def get_data_fn(param: str) -> str: return f"Data: {param}" + @pytest.mark.anyio async def test_resource_with_uri_params(self): """Test that a resource with URI parameters is automatically a template""" mcp = FastMCP() @@ -322,6 +344,7 @@ class TestServerResourceTemplates: def get_data() -> str: return "Data" + @pytest.mark.anyio async def test_resource_with_untyped_params(self): """Test that a resource with untyped parameters raises an error""" mcp = FastMCP() @@ -330,6 +353,7 @@ class TestServerResourceTemplates: def get_data(param) -> str: return "Data" + @pytest.mark.anyio async def test_resource_matching_params(self): """Test that a resource with matching URI and function parameters works""" mcp = FastMCP() @@ -343,6 +367,7 @@ class TestServerResourceTemplates: assert isinstance(result.contents[0], TextResourceContents) assert result.contents[0].text == "Data for test" + @pytest.mark.anyio async def test_resource_mismatched_params(self): """Test that mismatched parameters raise an error""" mcp = FastMCP() @@ -353,6 +378,7 @@ class TestServerResourceTemplates: def get_data(user: str) -> str: return f"Data for {user}" + @pytest.mark.anyio async def test_resource_multiple_params(self): """Test that multiple parameters work correctly""" mcp = FastMCP() @@ -368,6 +394,7 @@ class TestServerResourceTemplates: assert isinstance(result.contents[0], TextResourceContents) assert result.contents[0].text == "Data for cursor/fastmcp" + @pytest.mark.anyio async def test_resource_multiple_mismatched_params(self): """Test that mismatched parameters raise an error""" mcp = FastMCP() @@ -390,6 +417,7 @@ class TestServerResourceTemplates: assert isinstance(result.contents[0], TextResourceContents) assert result.contents[0].text == "Static data" + @pytest.mark.anyio async def test_template_to_resource_conversion(self): """Test that templates are properly converted to resources when accessed""" mcp = FastMCP() @@ -412,6 +440,7 @@ class TestServerResourceTemplates: class TestContextInjection: """Test context injection in tools.""" + @pytest.mark.anyio async def test_context_detection(self): """Test that context parameters are properly detected.""" mcp = FastMCP() @@ -422,6 +451,7 @@ class TestContextInjection: tool = mcp._tool_manager.add_tool(tool_with_context) assert tool.context_kwarg == "ctx" + @pytest.mark.anyio async def test_context_injection(self): """Test that context is properly injected into tool calls.""" mcp = FastMCP() @@ -439,6 +469,7 @@ class TestContextInjection: assert "Request" in content.text assert "42" in content.text + @pytest.mark.anyio async def test_async_context(self): """Test that context works in async functions.""" mcp = FastMCP() @@ -456,6 +487,7 @@ class TestContextInjection: assert "Async request" in content.text assert "42" in content.text + @pytest.mark.anyio async def test_context_logging(self): """Test that context logging methods work.""" mcp = FastMCP() @@ -475,6 +507,7 @@ class TestContextInjection: assert isinstance(content, TextContent) assert "Logged messages for test" in content.text + @pytest.mark.anyio async def test_optional_context(self): """Test that context is optional.""" mcp = FastMCP() @@ -490,6 +523,7 @@ class TestContextInjection: assert isinstance(content, TextContent) assert content.text == "42" + @pytest.mark.anyio async def test_context_resource_access(self): """Test that context can access resources.""" mcp = FastMCP() @@ -514,6 +548,7 @@ class TestContextInjection: class TestServerPrompts: """Test prompt functionality in FastMCP server.""" + @pytest.mark.anyio async def test_prompt_decorator(self): """Test that the prompt decorator registers prompts correctly.""" mcp = FastMCP() @@ -530,6 +565,7 @@ class TestServerPrompts: assert isinstance(content[0].content, TextContent) assert content[0].content.text == "Hello, world!" + @pytest.mark.anyio async def test_prompt_decorator_with_name(self): """Test prompt decorator with custom name.""" mcp = FastMCP() @@ -545,6 +581,7 @@ class TestServerPrompts: assert isinstance(content[0].content, TextContent) assert content[0].content.text == "Hello, world!" + @pytest.mark.anyio async def test_prompt_decorator_with_description(self): """Test prompt decorator with custom description.""" mcp = FastMCP() @@ -569,6 +606,7 @@ class TestServerPrompts: def fn() -> str: return "Hello, world!" + @pytest.mark.anyio async def test_list_prompts(self): """Test listing prompts through MCP protocol.""" mcp = FastMCP() @@ -590,6 +628,7 @@ class TestServerPrompts: assert prompt.arguments[1].name == "optional" assert prompt.arguments[1].required is False + @pytest.mark.anyio async def test_get_prompt(self): """Test getting a prompt through MCP protocol.""" mcp = FastMCP() @@ -607,6 +646,7 @@ class TestServerPrompts: assert isinstance(content, TextContent) assert content.text == "Hello, World!" + @pytest.mark.anyio async def test_get_prompt_with_resource(self): """Test getting a prompt that returns resource content.""" mcp = FastMCP() @@ -636,6 +676,7 @@ class TestServerPrompts: assert resource.text == "File contents" assert resource.mimeType == "text/plain" + @pytest.mark.anyio async def test_get_unknown_prompt(self): """Test error when getting unknown prompt.""" mcp = FastMCP() @@ -643,6 +684,7 @@ class TestServerPrompts: with pytest.raises(McpError, match="Unknown prompt"): await client.get_prompt("unknown") + @pytest.mark.anyio async def test_get_prompt_missing_args(self): """Test error when required arguments are missing.""" mcp = FastMCP() diff --git a/tests/server/fastmcp/test_tool_manager.py b/tests/server/fastmcp/test_tool_manager.py index 884059a..4adfc47 100644 --- a/tests/server/fastmcp/test_tool_manager.py +++ b/tests/server/fastmcp/test_tool_manager.py @@ -1,9 +1,10 @@ +import json import logging from typing import Optional import pytest from pydantic import BaseModel -import json + from mcp.server.fastmcp.exceptions import ToolError from mcp.server.fastmcp.tools import ToolManager @@ -27,6 +28,7 @@ class TestAddTools: assert tool.parameters["properties"]["a"]["type"] == "integer" assert tool.parameters["properties"]["b"]["type"] == "integer" + @pytest.mark.anyio async def test_async_function(self): """Test registering and running an async function.""" @@ -111,6 +113,7 @@ class TestAddTools: class TestCallTools: + @pytest.mark.anyio async def test_call_tool(self): def add(a: int, b: int) -> int: """Add two numbers.""" @@ -121,6 +124,7 @@ class TestCallTools: result = await manager.call_tool("add", {"a": 1, "b": 2}) assert result == 3 + @pytest.mark.anyio async def test_call_async_tool(self): async def double(n: int) -> int: """Double a number.""" @@ -131,6 +135,7 @@ class TestCallTools: result = await manager.call_tool("double", {"n": 5}) assert result == 10 + @pytest.mark.anyio async def test_call_tool_with_default_args(self): def add(a: int, b: int = 1) -> int: """Add two numbers.""" @@ -141,6 +146,7 @@ class TestCallTools: result = await manager.call_tool("add", {"a": 1}) assert result == 2 + @pytest.mark.anyio async def test_call_tool_with_missing_args(self): def add(a: int, b: int) -> int: """Add two numbers.""" @@ -151,11 +157,13 @@ class TestCallTools: with pytest.raises(ToolError): await manager.call_tool("add", {"a": 1}) + @pytest.mark.anyio async def test_call_unknown_tool(self): manager = ToolManager() with pytest.raises(ToolError): await manager.call_tool("unknown", {"a": 1}) + @pytest.mark.anyio async def test_call_tool_with_list_int_input(self): def sum_vals(vals: list[int]) -> int: return sum(vals) @@ -168,6 +176,7 @@ class TestCallTools: result = await manager.call_tool("sum_vals", {"vals": [1, 2, 3]}) assert result == 6 + @pytest.mark.anyio async def test_call_tool_with_list_str_or_str_input(self): def concat_strs(vals: list[str] | str) -> str: return vals if isinstance(vals, str) else "".join(vals) @@ -184,6 +193,7 @@ class TestCallTools: result = await manager.call_tool("concat_strs", {"vals": '"a"'}) assert result == '"a"' + @pytest.mark.anyio async def test_call_tool_with_complex_model(self): from mcp.server.fastmcp import Context @@ -212,6 +222,7 @@ class TestCallTools: class TestToolSchema: + @pytest.mark.anyio async def test_context_arg_excluded_from_schema(self): from mcp.server.fastmcp import Context @@ -229,7 +240,8 @@ class TestContextHandling: """Test context handling in the tool manager.""" def test_context_parameter_detection(self): - """Test that context parameters are properly detected in Tool.from_function().""" + """Test that context parameters are properly detected in + Tool.from_function().""" from mcp.server.fastmcp import Context def tool_with_context(x: int, ctx: Context) -> str: @@ -245,6 +257,7 @@ class TestContextHandling: tool = manager.add_tool(tool_without_context) assert tool.context_kwarg is None + @pytest.mark.anyio async def test_context_injection(self): """Test that context is properly injected during tool execution.""" from mcp.server.fastmcp import Context, FastMCP @@ -261,6 +274,7 @@ class TestContextHandling: result = await manager.call_tool("tool_with_context", {"x": 42}, context=ctx) assert result == "42" + @pytest.mark.anyio async def test_context_injection_async(self): """Test that context is properly injected in async tools.""" from mcp.server.fastmcp import Context, FastMCP @@ -277,6 +291,7 @@ class TestContextHandling: result = await manager.call_tool("async_tool", {"x": 42}, context=ctx) assert result == "42" + @pytest.mark.anyio async def test_context_optional(self): """Test that context is optional when calling tools.""" from mcp.server.fastmcp import Context @@ -290,6 +305,7 @@ class TestContextHandling: result = await manager.call_tool("tool_with_context", {"x": 42}) assert result == "42" + @pytest.mark.anyio async def test_context_error_handling(self): """Test error handling when context injection fails.""" from mcp.server.fastmcp import Context, FastMCP diff --git a/tests/test_examples.py b/tests/test_examples.py index 1a987cb..ea8a256 100644 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -1,50 +1,54 @@ """Tests for example servers""" + import pytest -from mcp.shared.memory import create_connected_server_and_client_session as client_session + +from mcp.shared.memory import ( + create_connected_server_and_client_session as client_session, +) + @pytest.mark.anyio async def test_simple_echo(): """Test the simple echo server""" from examples.fastmcp.simple_echo import mcp - + async with client_session(mcp._mcp_server) as client: result = await client.call_tool("echo", {"text": "hello"}) assert len(result.content) == 1 content = result.content[0] assert content.text == "hello" + @pytest.mark.anyio async def test_complex_inputs(): """Test the complex inputs server""" from examples.fastmcp.complex_inputs import mcp - + async with client_session(mcp._mcp_server) as client: - tank = { - "shrimp": [{"name": "bob"}, {"name": "alice"}] - } - result = await client.call_tool("name_shrimp", { - "tank": tank, - "extra_names": ["charlie"] - }) + tank = {"shrimp": [{"name": "bob"}, {"name": "alice"}]} + result = await client.call_tool( + "name_shrimp", {"tank": tank, "extra_names": ["charlie"]} + ) assert len(result.content) == 3 assert result.content[0].text == "bob" assert result.content[1].text == "alice" assert result.content[2].text == "charlie" + @pytest.mark.anyio async def test_desktop(): """Test the desktop server""" from examples.fastmcp.desktop import mcp - + async with client_session(mcp._mcp_server) as client: # Test the add function result = await client.call_tool("add", {"a": 1, "b": 2}) assert len(result.content) == 1 content = result.content[0] assert content.text == "3" - + # Test the desktop resource result = await client.read_resource("dir://desktop") assert len(result.contents) == 1 content = result.contents[0] - assert isinstance(content.text, str) \ No newline at end of file + assert isinstance(content.text, str) diff --git a/tests/test_types.py b/tests/test_types.py index c3981ad..a39d334 100644 --- a/tests/test_types.py +++ b/tests/test_types.py @@ -1,3 +1,5 @@ +import pytest + from mcp.types import ( LATEST_PROTOCOL_VERSION, ClientRequest, @@ -6,7 +8,8 @@ from mcp.types import ( ) -def test_jsonrpc_request(): +@pytest.mark.anyio +async def test_jsonrpc_request(): json_data = { "jsonrpc": "2.0", "id": 1, diff --git a/uv.lock b/uv.lock index dcc5cc1..54c5b98 100644 --- a/uv.lock +++ b/uv.lock @@ -216,7 +216,6 @@ rich = [ dev = [ { name = "pyright" }, { name = "pytest" }, - { name = "pytest-asyncio" }, { name = "pytest-flakefinder" }, { name = "pytest-xdist" }, { name = "ruff" }, @@ -241,7 +240,6 @@ requires-dist = [ dev = [ { name = "pyright", specifier = ">=1.1.378" }, { name = "pytest", specifier = ">=8.3.3" }, - { name = "pytest-asyncio", specifier = ">=0.24.0" }, { name = "pytest-flakefinder", specifier = ">=1.1.0" }, { name = "pytest-xdist", specifier = ">=3.6.1" }, { name = "ruff", specifier = ">=0.6.9" }, @@ -526,18 +524,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6b/77/7440a06a8ead44c7757a64362dd22df5760f9b12dc5f11b6188cd2fc27a0/pytest-8.3.3-py3-none-any.whl", hash = "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2", size = 342341 }, ] -[[package]] -name = "pytest-asyncio" -version = "0.24.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pytest" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/52/6d/c6cf50ce320cf8611df7a1254d86233b3df7cc07f9b5f5cbcb82e08aa534/pytest_asyncio-0.24.0.tar.gz", hash = "sha256:d081d828e576d85f875399194281e92bf8a68d60d72d1a2faf2feddb6c46b276", size = 49855 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/96/31/6607dab48616902f76885dfcf62c08d929796fc3b2d2318faf9fd54dbed9/pytest_asyncio-0.24.0-py3-none-any.whl", hash = "sha256:a811296ed596b69bf0b6f3dc40f83bcaf341b155a269052d82efa2b25ac7037b", size = 18024 }, -] - [[package]] name = "pytest-flakefinder" version = "1.1.0"