mirror of
https://github.com/aljazceru/mcp-python-sdk.git
synced 2025-12-18 14:34:27 +01:00
style: Fix imports and line length formatting
This commit is contained in:
@@ -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")
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
from mcp.server.fastmcp import FastMCP
|
||||
|
||||
|
||||
# Create an MCP server
|
||||
mcp = FastMCP("Demo")
|
||||
|
||||
|
||||
@@ -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"])
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ FastMCP Echo Server
|
||||
|
||||
from mcp.server.fastmcp import FastMCP
|
||||
|
||||
|
||||
# Create server
|
||||
mcp = FastMCP("Echo Server")
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -2,6 +2,5 @@
|
||||
|
||||
from .cli import app
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app()
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from .lowlevel import Server, NotificationOptions
|
||||
from .fastmcp import FastMCP
|
||||
from .lowlevel import NotificationOptions, Server
|
||||
|
||||
__all__ = ["Server", "FastMCP", "NotificationOptions"]
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"""Resource manager functionality."""
|
||||
|
||||
from typing import Callable
|
||||
from collections.abc import Iterable
|
||||
|
||||
from pydantic import AnyUrl
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
from .server import Server, NotificationOptions
|
||||
from .server import NotificationOptions, Server
|
||||
|
||||
__all__ = ["Server", "NotificationOptions"]
|
||||
|
||||
@@ -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."""
|
||||
|
||||
|
||||
@@ -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."""
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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."""
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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."""
|
||||
|
||||
|
||||
@@ -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."""
|
||||
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
assert isinstance(content.text, str)
|
||||
|
||||
@@ -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,
|
||||
|
||||
14
uv.lock
generated
14
uv.lock
generated
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user