Update tests

This commit is contained in:
Jeremiah Lowin 2025-06-19 14:05:19 -04:00
commit 2e31b2704b
13 changed files with 844 additions and 280 deletions

510
docs/servers/middleware.mdx Normal file
View file

@ -0,0 +1,510 @@
---
title: MCP Middleware
sidebarTitle: Middleware
description: Add cross-cutting functionality to your MCP server with middleware that can inspect, modify, and respond to all MCP requests and responses.
icon: layers
---
import { VersionBadge } from "/snippets/version-badge.mdx"
<VersionBadge version="2.8.0" />
MCP middleware is a powerful concept that allows you to add cross-cutting functionality to your FastMCP server. Unlike traditional web middleware, MCP middleware is designed specifically for the Model Context Protocol, providing hooks for different types of MCP operations like tool calls, resource reads, and prompt requests.
<Warning>
MCP middleware is a FastMCP-specific concept and is not part of the official MCP protocol specification. This middleware system is designed to work with FastMCP servers and may not be compatible with other MCP implementations.
</Warning>
## What is MCP Middleware?
MCP middleware lets you intercept and modify MCP requests and responses as they flow through your server. Unlike traditional HTTP middleware that operates on request/response pairs, MCP middleware is aware of the specific MCP protocol operations and can provide targeted hooks for different types of interactions.
Common use cases for MCP middleware include:
- **Authentication and Authorization**: Verify client permissions before executing operations
- **Logging and Monitoring**: Track usage patterns and performance metrics
- **Rate Limiting**: Control request frequency per client or operation type
- **Request/Response Transformation**: Modify data before it reaches tools or after it leaves
- **Caching**: Store frequently requested data to improve performance
- **Error Handling**: Provide consistent error responses across your server
## How MCP Middleware Works
MCP middleware operates on a pipeline model where each middleware can:
1. **Inspect the incoming request** and its context
2. **Modify the request** before passing it to the next middleware or handler
3. **Execute the next middleware/handler** in the chain
4. **Inspect and modify the response** before returning it
5. **Handle errors** that occur during processing
The middleware system provides specialized hooks for different MCP operations:
- `on_message`: Called for all MCP messages (requests and notifications)
- `on_request`: Called specifically for MCP requests (that expect responses)
- `on_notification`: Called specifically for MCP notifications (fire-and-forget)
- `on_call_tool`: Called when tools are being executed
- `on_read_resource`: Called when resources are being read
- `on_get_prompt`: Called when prompts are being retrieved
- `on_list_tools`: Called when listing available tools
- `on_list_resources`: Called when listing available resources
- `on_list_resource_templates`: Called when listing resource templates
- `on_list_prompts`: Called when listing available prompts
<Tip>
The middleware hook system is designed to be extensible. As FastMCP evolves and new MCP operations are added, additional hooks will be introduced to provide fine-grained control over new functionality.
</Tip>
## Creating Middleware
### Basic Middleware Structure
MCP middleware is implemented by subclassing the `MCPMiddleware` base class and overriding the hooks you need:
```python
from fastmcp import FastMCP
from fastmcp.server.middleware import MCPMiddleware, MiddlewareContext
class LoggingMiddleware(MCPMiddleware):
"""Middleware that logs all MCP operations."""
async def on_message(self, context: MiddlewareContext, call_next):
"""Called for all MCP messages."""
print(f"Processing {context.method} from {context.source}")
# Call the next middleware/handler in the chain
result = await call_next(context)
print(f"Completed {context.method}")
return result
async def on_call_tool(self, context: MiddlewareContext, call_next):
"""Called specifically for tool calls."""
tool_name = context.message.name
print(f"Calling tool: {tool_name}")
result = await call_next(context)
print(f"Tool {tool_name} completed")
return result
# Add middleware to your server
mcp = FastMCP("MyServer")
mcp.add_middleware(LoggingMiddleware())
```
### Middleware Context
The `MiddlewareContext` object provides access to information about the current request:
```python
class InspectionMiddleware(MCPMiddleware):
async def on_request(self, context: MiddlewareContext, call_next):
# Access request information
method = context.method # e.g., "tools/call"
source = context.source # "client" or "server"
message_type = context.type # "request" or "notification"
timestamp = context.timestamp # When the request was received
message = context.message # The actual MCP message
fastmcp_context = context.fastmcp_context # FastMCP Context object (if available)
# Continue processing
return await call_next(context)
```
### Middleware Hooks
Each middleware hook receives a `MiddlewareContext` and a `call_next` function. The hooks are organized in a hierarchy:
1. **`on_message`**: The broadest hook, called for all MCP messages
2. **`on_request`** / **`on_notification`**: Called based on message type
3. **Operation-specific hooks**: Called for specific MCP operations
```python
class ComprehensiveMiddleware(MCPMiddleware):
async def on_message(self, context: MiddlewareContext, call_next):
"""Called for ALL messages (requests and notifications)."""
print(f"Message: {context.method}")
return await call_next(context)
async def on_request(self, context: MiddlewareContext, call_next):
"""Called only for requests (messages that expect responses)."""
print(f"Request: {context.method}")
return await call_next(context)
async def on_call_tool(self, context: MiddlewareContext, call_next):
"""Called only for tool execution requests."""
tool_name = context.message.name
print(f"Executing tool: {tool_name}")
return await call_next(context)
```
## Middleware Examples
### Authentication Middleware
```python
from fastmcp.server.middleware import MCPMiddleware, MiddlewareContext
from fastmcp.exceptions import ToolError
class AuthenticationMiddleware(MCPMiddleware):
def __init__(self, required_token: str):
self.required_token = required_token
async def on_request(self, context: MiddlewareContext, call_next):
"""Verify authentication for all requests."""
# Check if this is an HTTP request with headers
if hasattr(context, 'fastmcp_context') and context.fastmcp_context:
try:
# Access HTTP request if available
request = context.fastmcp_context.get_http_request()
auth_header = request.headers.get("Authorization")
if not auth_header or not auth_header.startswith("Bearer "):
raise ToolError("Missing or invalid authorization header")
token = auth_header.split(" ", 1)[1]
if token != self.required_token:
raise ToolError("Invalid authentication token")
except Exception:
# If HTTP request is not available, continue without auth
pass
return await call_next(context)
# Usage
mcp = FastMCP("SecureServer")
mcp.add_middleware(AuthenticationMiddleware("secret-token-123"))
```
### Performance Monitoring Middleware
```python
import time
import logging
class PerformanceMiddleware(MCPMiddleware):
def __init__(self):
self.logger = logging.getLogger("performance")
async def on_call_tool(self, context: MiddlewareContext, call_next):
"""Monitor tool execution performance."""
tool_name = context.message.name
start_time = time.time()
try:
result = await call_next(context)
execution_time = time.time() - start_time
self.logger.info(
f"Tool {tool_name} completed in {execution_time:.3f}s"
)
return result
except Exception as e:
execution_time = time.time() - start_time
self.logger.error(
f"Tool {tool_name} failed after {execution_time:.3f}s: {e}"
)
raise
```
### Request/Response Transformation Middleware
```python
class TransformationMiddleware(MCPMiddleware):
async def on_call_tool(self, context: MiddlewareContext, call_next):
"""Transform tool arguments and results."""
# Access and modify tool arguments
if hasattr(context.message, 'arguments'):
args = context.message.arguments or {}
# Example: Add a timestamp to all tool calls
args['_middleware_timestamp'] = context.timestamp.isoformat()
# Create a modified context
modified_context = context.copy(
message=context.message.model_copy(update={'arguments': args})
)
else:
modified_context = context
# Execute with modified context
result = await call_next(modified_context)
# Transform the result if needed
if hasattr(result, 'content'):
# Example: Add metadata to tool results
if result.content and len(result.content) > 0:
original_content = result.content[0].text
enhanced_content = f"[Processed at {context.timestamp}]\n{original_content}"
result.content[0].text = enhanced_content
return result
```
### Rate Limiting Middleware
```python
import asyncio
from collections import defaultdict
from datetime import datetime, timedelta
class RateLimitMiddleware(MCPMiddleware):
def __init__(self, max_requests: int = 100, window_minutes: int = 1):
self.max_requests = max_requests
self.window = timedelta(minutes=window_minutes)
self.requests = defaultdict(list) # client_id -> [timestamps]
async def on_request(self, context: MiddlewareContext, call_next):
"""Implement rate limiting per client."""
# Get client identifier (you may need to implement this based on your auth)
client_id = getattr(context.fastmcp_context, 'client_id', 'anonymous')
now = datetime.now()
# Clean old requests
self.requests[client_id] = [
timestamp for timestamp in self.requests[client_id]
if now - timestamp < self.window
]
# Check rate limit
if len(self.requests[client_id]) >= self.max_requests:
raise ToolError(
f"Rate limit exceeded: {self.max_requests} requests per "
f"{self.window.total_seconds()/60:.0f} minutes"
)
# Record this request
self.requests[client_id].append(now)
return await call_next(context)
```
## Adding Middleware to Your Server
### Single Middleware
```python
from fastmcp import FastMCP
mcp = FastMCP("MyServer")
# Add a single middleware instance
logging_middleware = LoggingMiddleware()
mcp.add_middleware(logging_middleware)
```
### Multiple Middleware
Middleware is executed in the order it's added to the server:
```python
mcp = FastMCP("MyServer")
# Add multiple middleware - they execute in order
mcp.add_middleware(AuthenticationMiddleware("secret-token"))
mcp.add_middleware(PerformanceMiddleware())
mcp.add_middleware(LoggingMiddleware())
# Request flow:
# 1. AuthenticationMiddleware.on_request()
# 2. PerformanceMiddleware.on_request()
# 3. LoggingMiddleware.on_request()
# 4. Actual tool/resource handler
# 5. LoggingMiddleware response processing
# 6. PerformanceMiddleware response processing
# 7. AuthenticationMiddleware response processing
```
## Advanced Patterns
### Conditional Middleware
```python
class ConditionalMiddleware(MCPMiddleware):
def __init__(self, condition_func):
self.should_process = condition_func
async def on_call_tool(self, context: MiddlewareContext, call_next):
"""Only process certain tools."""
if not self.should_process(context.message.name):
# Skip processing for this tool
return await call_next(context)
# Apply middleware logic
print(f"Processing tool: {context.message.name}")
return await call_next(context)
# Usage
def only_expensive_tools(tool_name: str) -> bool:
return tool_name in ["complex_analysis", "heavy_computation"]
mcp.add_middleware(ConditionalMiddleware(only_expensive_tools))
```
### Middleware with State
```python
class StatefulMiddleware(MCPMiddleware):
def __init__(self):
self.call_count = 0
self.tools_used = set()
async def on_call_tool(self, context: MiddlewareContext, call_next):
"""Track usage statistics."""
self.call_count += 1
self.tools_used.add(context.message.name)
print(f"Total calls: {self.call_count}, Unique tools: {len(self.tools_used)}")
return await call_next(context)
def get_stats(self):
return {
"total_calls": self.call_count,
"unique_tools": len(self.tools_used),
"tools_used": list(self.tools_used)
}
```
### Error Handling Middleware
```python
class ErrorHandlingMiddleware(MCPMiddleware):
async def on_call_tool(self, context: MiddlewareContext, call_next):
"""Provide consistent error handling."""
try:
return await call_next(context)
except ToolError:
# Re-raise ToolErrors as-is
raise
except Exception as e:
# Log the error and convert to a user-friendly message
logging.error(f"Tool {context.message.name} failed: {e}")
raise ToolError(f"Tool execution failed: {str(e)}")
```
## Best Practices
### Performance Considerations
1. **Keep middleware lightweight**: Avoid heavy computations in middleware
2. **Use async operations**: Don't block the event loop with synchronous operations
3. **Cache when possible**: Store frequently accessed data to avoid repeated lookups
```python
class EfficientMiddleware(MCPMiddleware):
def __init__(self):
self._cache = {}
async def on_call_tool(self, context: MiddlewareContext, call_next):
"""Example of efficient middleware with caching."""
# Check cache first
cache_key = f"{context.message.name}:{hash(str(context.message.arguments))}"
if cache_key in self._cache:
print("Returning cached result")
return self._cache[cache_key]
# Execute and cache result
result = await call_next(context)
self._cache[cache_key] = result
return result
```
### Error Handling
1. **Always call `call_next`**: Unless you're intentionally stopping the chain
2. **Handle exceptions appropriately**: Don't let middleware errors break the entire request
3. **Use `ToolError` for client-facing errors**: Keep internal errors internal
```python
class RobustMiddleware(MCPMiddleware):
async def on_request(self, context: MiddlewareContext, call_next):
"""Robust error handling example."""
try:
# Middleware logic here
return await call_next(context)
except ToolError:
# Client-facing errors should be re-raised
raise
except Exception as e:
# Log internal errors but don't expose details
logging.error(f"Middleware error: {e}")
# Optionally continue without middleware processing
return await call_next(context)
```
### Testing Middleware
```python
import pytest
from fastmcp import FastMCP, Client
@pytest.mark.asyncio
async def test_logging_middleware():
"""Test middleware functionality."""
# Create server with middleware
mcp = FastMCP("TestServer")
logging_middleware = LoggingMiddleware()
mcp.add_middleware(logging_middleware)
@mcp.tool
def test_tool(x: int) -> int:
return x * 2
# Test with client
async with Client(mcp) as client:
result = await client.call_tool("test_tool", {"x": 5})
assert result == 10
# Verify middleware was called
# (You'll need to add tracking to your middleware for testing)
```
## Server Composition and Middleware
When using [Server Composition](/servers/composition) with `mount` or `import_server`, middleware behavior follows these rules:
1. **Parent server middleware** runs for all requests, including those routed to mounted servers
2. **Mounted server middleware** only runs for requests handled by that specific server
3. **Middleware order** is preserved within each server
```python
# Parent server with middleware
parent = FastMCP("Parent")
parent.add_middleware(AuthenticationMiddleware("token"))
# Child server with its own middleware
child = FastMCP("Child")
child.add_middleware(LoggingMiddleware())
@child.tool
def child_tool() -> str:
return "from child"
# Mount the child server
parent.mount(child, prefix="child")
# Request to "child_tool" will:
# 1. Run parent's AuthenticationMiddleware
# 2. Route to child server
# 3. Run child's LoggingMiddleware
# 4. Execute child_tool
```
This allows you to create layered middleware architectures where parent servers handle cross-cutting concerns like authentication, while child servers focus on domain-specific middleware.
<Tip>
When debugging middleware issues in composed servers, remember that both parent and child middleware may be executing. Use detailed logging to trace the middleware execution path.
</Tip>

View file

@ -27,7 +27,7 @@ class PromptManager:
mask_error_details: bool | None = None,
):
self._prompts: dict[str, Prompt] = {}
self._mounted_sources: list[MountedServer] = []
self._mounted_servers: list[MountedServer] = []
self.mask_error_details = mask_error_details or settings.mask_error_details
# Default to "warn" if None is provided
@ -44,7 +44,7 @@ class PromptManager:
def mount(self, server: MountedServer) -> None:
"""Adds a mounted server as a source for prompts."""
self._mounted_sources.append(server)
self._mounted_servers.append(server)
async def _load_prompts(self, *, via_server: bool = False) -> dict[str, Prompt]:
"""
@ -56,7 +56,7 @@ class PromptManager:
"""
all_prompts: dict[str, Prompt] = {}
for mounted in self._mounted_sources:
for mounted in self._mounted_servers:
try:
if via_server:
# Use the server-to-server filtered path
@ -188,12 +188,16 @@ class PromptManager:
raise PromptError(f"Error rendering prompt {name!r}: {e}") from e
# 2. Check mounted servers using the filtered protocol path.
for mounted in reversed(self._mounted_sources):
if mounted.prefix and name.startswith(f"{mounted.prefix}_"):
name_on_child = name.removeprefix(f"{mounted.prefix}_")
try:
return await mounted.server._get_prompt(name_on_child, arguments)
except NotFoundError:
for mounted in reversed(self._mounted_servers):
prompt_key = name
if mounted.prefix:
if name.startswith(f"{mounted.prefix}_"):
prompt_key = name.removeprefix(f"{mounted.prefix}_")
else:
continue
try:
return await mounted.server._get_prompt(prompt_key, arguments)
except NotFoundError:
continue
raise NotFoundError(f"Unknown prompt: {name}")

View file

@ -43,7 +43,7 @@ class ResourceManager:
"""
self._resources: dict[str, Resource] = {}
self._templates: dict[str, ResourceTemplate] = {}
self._mounted_sources: list[MountedServer] = []
self._mounted_servers: list[MountedServer] = []
self.mask_error_details = mask_error_details or settings.mask_error_details
# Default to "warn" if None is provided
@ -59,7 +59,7 @@ class ResourceManager:
def mount(self, server: MountedServer) -> None:
"""Adds a mounted server as a source for resources and templates."""
self._mounted_sources.append(server)
self._mounted_servers.append(server)
async def get_resources(self) -> dict[str, Resource]:
"""Get all registered resources, keyed by URI."""
@ -79,7 +79,7 @@ class ResourceManager:
"""
all_resources: dict[str, Resource] = {}
for mounted in self._mounted_sources:
for mounted in self._mounted_servers:
try:
if via_server:
# Use the server-to-server filtered path
@ -129,7 +129,7 @@ class ResourceManager:
"""
all_templates: dict[str, ResourceTemplate] = {}
for mounted in self._mounted_sources:
for mounted in self._mounted_servers:
try:
if via_server:
# Use the server-to-server filtered path
@ -457,8 +457,8 @@ class ResourceManager:
) from e
# 1b. Check local templates if not found in concrete resources
for template in self._templates.values():
if params := match_uri_template(uri_str, template.uri_template):
for key, template in self._templates.items():
if params := match_uri_template(uri_str, key):
try:
resource = await template.create_resource(uri_str, params=params)
return await resource.read()
@ -483,29 +483,28 @@ class ResourceManager:
# 2. Check mounted servers using the filtered protocol path.
from fastmcp.server.server import has_resource_prefix, remove_resource_prefix
for mounted in reversed(self._mounted_sources):
resource_uri = uri_str
for mounted in reversed(self._mounted_servers):
key = uri_str
try:
if mounted.prefix:
# If server has a prefix, check if URI matches and strip prefix
if has_resource_prefix(
resource_uri,
key,
mounted.prefix,
mounted.resource_prefix_format,
):
resource_uri = remove_resource_prefix(
resource_uri,
key = remove_resource_prefix(
key,
mounted.prefix,
mounted.resource_prefix_format,
)
else:
continue
result = await mounted.server._read_resource(resource_uri)
# Extract content from the first ReadResourceContents
if result and len(result) > 0:
try:
result = await mounted.server._read_resource(key)
return result[0].content
raise NotFoundError(f"Resource {uri_str!r} returned empty content")
except NotFoundError:
continue
except NotFoundError:
continue

View file

@ -62,6 +62,9 @@ class ResourceTemplate(FastMCPComponent):
description="JSON schema for function parameters"
)
def __repr__(self) -> str:
return f"{self.__class__.__name__}(uri_template={self.uri_template!r}, name={self.name!r}, description={self.description!r}, tags={self.tags})"
@staticmethod
def from_function(
fn: Callable[..., Any],

View file

@ -28,7 +28,7 @@ class ToolManager:
mask_error_details: bool | None = None,
):
self._tools: dict[str, Tool] = {}
self._mounted_sources: list[MountedServer] = []
self._mounted_servers: list[MountedServer] = []
self.mask_error_details = mask_error_details or settings.mask_error_details
# Default to "warn" if None is provided
@ -45,7 +45,7 @@ class ToolManager:
def mount(self, server: MountedServer) -> None:
"""Adds a mounted server as a source for tools."""
self._mounted_sources.append(server)
self._mounted_servers.append(server)
async def _load_tools(self, *, via_server: bool = False) -> dict[str, Tool]:
"""
@ -57,7 +57,7 @@ class ToolManager:
"""
all_tools: dict[str, Tool] = {}
for mounted in self._mounted_sources:
for mounted in self._mounted_servers:
try:
if via_server:
# Use the server-to-server filtered path
@ -201,12 +201,16 @@ class ToolManager:
raise ToolError(f"Error calling tool {key!r}: {e}") from e
# 2. Check mounted servers using the filtered protocol path.
for mounted in reversed(self._mounted_sources):
if mounted.prefix and key.startswith(f"{mounted.prefix}_"):
key_on_child = key.removeprefix(f"{mounted.prefix}_")
try:
return await mounted.server._call_tool(key_on_child, arguments)
except NotFoundError:
for mounted in reversed(self._mounted_servers):
tool_key = key
if mounted.prefix:
if key.startswith(f"{mounted.prefix}_"):
tool_key = key.removeprefix(f"{mounted.prefix}_")
else:
continue
try:
return await mounted.server._call_tool(tool_key, arguments)
except NotFoundError:
continue
raise NotFoundError(f"Tool {key!r} not found.")

View file

@ -10,7 +10,7 @@ from fastmcp.prompts.prompt_manager import PromptManager
class TestPromptManager:
def test_add_prompt(self):
async def test_add_prompt(self):
"""Test adding a prompt to the manager."""
def fn() -> str:
@ -20,9 +20,9 @@ class TestPromptManager:
prompt = Prompt.from_function(fn)
added = manager.add_prompt(prompt)
assert added == prompt
assert manager.get_prompt("fn") == prompt
assert await manager.get_prompt("fn") == prompt
def test_add_duplicate_prompt(self, caplog):
async def test_add_duplicate_prompt(self, caplog):
"""Test adding the same prompt twice."""
def fn() -> str:
@ -35,7 +35,7 @@ class TestPromptManager:
assert first == second
assert "Prompt already exists" in caplog.text
def test_disable_warn_on_duplicate_prompts(self, caplog):
async def test_disable_warn_on_duplicate_prompts(self, caplog):
"""Test disabling warning on duplicate prompts."""
def fn() -> str:
@ -48,7 +48,7 @@ class TestPromptManager:
assert first == second
assert "Prompt already exists" not in caplog.text
def test_warn_on_duplicate_prompts(self, caplog):
async def test_warn_on_duplicate_prompts(self, caplog):
"""Test warning on duplicate prompts."""
manager = PromptManager(duplicate_behavior="warn")
@ -62,9 +62,9 @@ class TestPromptManager:
assert "Prompt already exists: test_prompt" in caplog.text
# Should have the prompt
assert manager.get_prompt("test_prompt") is not None
assert await manager.get_prompt("test_prompt") is not None
def test_error_on_duplicate_prompts(self):
async def test_error_on_duplicate_prompts(self):
"""Test error on duplicate prompts."""
manager = PromptManager(duplicate_behavior="error")
@ -78,7 +78,7 @@ class TestPromptManager:
with pytest.raises(ValueError, match="Prompt already exists: test_prompt"):
manager.add_prompt(prompt)
def test_replace_duplicate_prompts(self):
async def test_replace_duplicate_prompts(self):
"""Test replacing duplicate prompts."""
manager = PromptManager(duplicate_behavior="replace")
@ -95,12 +95,12 @@ class TestPromptManager:
manager.add_prompt(prompt2)
# Should have replaced with the new prompt
prompt = manager.get_prompt("test_prompt")
prompt = await manager.get_prompt("test_prompt")
assert prompt is not None
assert isinstance(prompt, FunctionPrompt)
assert prompt.fn.__name__ == "replacement_fn"
def test_ignore_duplicate_prompts(self):
async def test_ignore_duplicate_prompts(self):
"""Test ignoring duplicate prompts."""
manager = PromptManager(duplicate_behavior="ignore")
@ -117,7 +117,7 @@ class TestPromptManager:
result = manager.add_prompt(prompt2)
# Should keep the original
prompt = manager.get_prompt("test_prompt")
prompt = await manager.get_prompt("test_prompt")
assert prompt is not None
assert isinstance(prompt, FunctionPrompt)
assert prompt.fn.__name__ == "original_fn"
@ -125,7 +125,7 @@ class TestPromptManager:
assert isinstance(result, FunctionPrompt)
assert result.fn.__name__ == "original_fn"
def test_get_prompts(self):
async def test_get_prompts(self):
"""Test retrieving all prompts."""
def fn1() -> str:
@ -139,7 +139,7 @@ class TestPromptManager:
prompt2 = Prompt.from_function(fn2)
manager.add_prompt(prompt1)
manager.add_prompt(prompt2)
prompts = manager.get_prompts()
prompts = await manager.get_prompts()
assert len(prompts) == 2
assert prompts["fn1"] == prompt1
assert prompts["fn2"] == prompt2
@ -270,7 +270,7 @@ class TestRenderPrompt:
class TestPromptTags:
"""Test functionality related to prompt tags."""
def test_add_prompt_with_tags(self):
async def test_add_prompt_with_tags(self):
"""Test adding a prompt with tags."""
def greeting() -> str:
@ -280,11 +280,11 @@ class TestPromptTags:
prompt = Prompt.from_function(greeting, tags={"greeting", "simple"})
manager.add_prompt(prompt)
prompt = manager.get_prompt("greeting")
prompt = await manager.get_prompt("greeting")
assert prompt is not None
assert prompt.tags == {"greeting", "simple"}
def test_add_prompt_with_empty_tags(self):
async def test_add_prompt_with_empty_tags(self):
"""Test adding a prompt with empty tags."""
def greeting() -> str:
@ -294,11 +294,11 @@ class TestPromptTags:
prompt = Prompt.from_function(greeting, tags=set())
manager.add_prompt(prompt)
prompt = manager.get_prompt("greeting")
prompt = await manager.get_prompt("greeting")
assert prompt is not None
assert prompt.tags == set()
def test_add_prompt_with_none_tags(self):
async def test_add_prompt_with_none_tags(self):
"""Test adding a prompt with None tags."""
def greeting() -> str:
@ -308,11 +308,11 @@ class TestPromptTags:
prompt = Prompt.from_function(greeting, tags=None)
manager.add_prompt(prompt)
prompt = manager.get_prompt("greeting")
prompt = await manager.get_prompt("greeting")
assert prompt is not None
assert prompt.tags == set()
def test_list_prompts_with_tags(self):
async def test_list_prompts_with_tags(self):
"""Test listing prompts with specific tags."""
def greeting() -> str:
@ -332,13 +332,12 @@ class TestPromptTags:
)
# Filter prompts by tags
simple_prompts = [
p for p in manager.get_prompts().values() if "simple" in p.tags
]
prompts = await manager.get_prompts()
simple_prompts = [p for p in prompts.values() if "simple" in p.tags]
assert len(simple_prompts) == 2
assert {p.name for p in simple_prompts} == {"greeting", "summary"}
nlp_prompts = [p for p in manager.get_prompts().values() if "nlp" in p.tags]
nlp_prompts = [p for p in prompts.values() if "nlp" in p.tags]
assert len(nlp_prompts) == 1
assert nlp_prompts[0].name == "summary"

View file

@ -33,7 +33,7 @@ def temp_file():
class TestResourceManager:
"""Test ResourceManager functionality."""
def test_add_resource(self, temp_file: Path):
async def test_add_resource(self, temp_file: Path):
"""Test adding a resource."""
manager = ResourceManager()
file_url = "file://test-resource"
@ -45,10 +45,11 @@ class TestResourceManager:
added = manager.add_resource(resource)
assert added == resource
# Get the actual key from the resource manager
assert len(manager.get_resources()) == 1
assert resource in manager.get_resources().values()
resources = await manager.get_resources()
assert len(resources) == 1
assert resource in resources.values()
def test_add_duplicate_resource(self, temp_file: Path):
async def test_add_duplicate_resource(self, temp_file: Path):
"""Test adding the same resource twice."""
manager = ResourceManager()
file_url = "file://test-resource"
@ -61,10 +62,11 @@ class TestResourceManager:
second = manager.add_resource(resource)
assert first == second
# Check the resource is there
assert len(manager.get_resources()) == 1
assert resource in manager.get_resources().values()
resources = await manager.get_resources()
assert len(resources) == 1
assert resource in resources.values()
def test_warn_on_duplicate_resources(self, temp_file: Path, caplog):
async def test_warn_on_duplicate_resources(self, temp_file: Path, caplog):
"""Test warning on duplicate resources."""
manager = ResourceManager(duplicate_behavior="warn")
@ -80,10 +82,11 @@ class TestResourceManager:
assert "Resource already exists" in caplog.text
# Should have the resource
assert len(manager.get_resources()) == 1
assert resource in manager.get_resources().values()
resources = await manager.get_resources()
assert len(resources) == 1
assert resource in resources.values()
def test_disable_warn_on_duplicate_resources(self, temp_file: Path, caplog):
async def test_disable_warn_on_duplicate_resources(self, temp_file: Path, caplog):
"""Test disabling warning on duplicate resources."""
manager = ResourceManager(duplicate_behavior="ignore")
resource = FileResource(
@ -95,7 +98,7 @@ class TestResourceManager:
manager.add_resource(resource)
assert "Resource already exists" not in caplog.text
def test_error_on_duplicate_resources(self, temp_file: Path):
async def test_error_on_duplicate_resources(self, temp_file: Path):
"""Test error on duplicate resources."""
manager = ResourceManager(duplicate_behavior="error")
@ -110,7 +113,7 @@ class TestResourceManager:
with pytest.raises(ValueError, match="Resource already exists"):
manager.add_resource(resource)
def test_replace_duplicate_resources(self, temp_file: Path):
async def test_replace_duplicate_resources(self, temp_file: Path):
"""Test replacing duplicate resources."""
manager = ResourceManager(duplicate_behavior="replace")
@ -131,11 +134,12 @@ class TestResourceManager:
manager.add_resource(resource2)
# Should have replaced with the new resource
resources = list(manager.get_resources().values())
assert len(resources) == 1
assert resources[0].name == "replacement"
resources = await manager.get_resources()
resource_list = list(resources.values())
assert len(resource_list) == 1
assert resource_list[0].name == "replacement"
def test_ignore_duplicate_resources(self, temp_file: Path):
async def test_ignore_duplicate_resources(self, temp_file: Path):
"""Test ignoring duplicate resources."""
manager = ResourceManager(duplicate_behavior="ignore")
@ -156,13 +160,14 @@ class TestResourceManager:
result = manager.add_resource(resource2)
# Should keep the original
resources = list(manager.get_resources().values())
assert len(resources) == 1
assert resources[0].name == "original"
resources = await manager.get_resources()
resource_list = list(resources.values())
assert len(resource_list) == 1
assert resource_list[0].name == "original"
# Result should be the original resource
assert result.name == "original"
def test_warn_on_duplicate_templates(self, caplog):
async def test_warn_on_duplicate_templates(self, caplog):
"""Test warning on duplicate templates."""
manager = ResourceManager(duplicate_behavior="warn")
@ -180,9 +185,10 @@ class TestResourceManager:
assert "Template already exists" in caplog.text
# Should have the template
assert manager.get_resource_templates() == {"test://{id}": template}
templates = await manager.get_resource_templates()
assert templates == {"test://{id}": template}
def test_error_on_duplicate_templates(self):
async def test_error_on_duplicate_templates(self):
"""Test error on duplicate templates."""
manager = ResourceManager(duplicate_behavior="error")
@ -200,7 +206,7 @@ class TestResourceManager:
with pytest.raises(ValueError, match="Template already exists"):
manager.add_template(template)
def test_replace_duplicate_templates(self):
async def test_replace_duplicate_templates(self):
"""Test replacing duplicate templates."""
manager = ResourceManager(duplicate_behavior="replace")
@ -226,11 +232,12 @@ class TestResourceManager:
manager.add_template(template2)
# Should have replaced with the new template
templates = list(manager.get_resource_templates().values())
templates_dict = await manager.get_resource_templates()
templates = list(templates_dict.values())
assert len(templates) == 1
assert templates[0].name == "replacement"
def test_ignore_duplicate_templates(self):
async def test_ignore_duplicate_templates(self):
"""Test ignoring duplicate templates."""
manager = ResourceManager(duplicate_behavior="ignore")
@ -256,7 +263,8 @@ class TestResourceManager:
result = manager.add_template(template2)
# Should keep the original
templates = list(manager.get_resource_templates().values())
templates_dict = await manager.get_resource_templates()
templates = list(templates_dict.values())
assert len(templates) == 1
assert templates[0].name == "original"
# Result should be the original template
@ -299,7 +307,7 @@ class TestResourceManager:
with pytest.raises(NotFoundError, match="Unknown resource"):
await manager.get_resource(AnyUrl("unknown://test"))
def test_get_resources(self, temp_file: Path):
async def test_get_resources(self, temp_file: Path):
"""Test retrieving all resources."""
manager = ResourceManager()
file_url1 = "file://test-resource1"
@ -316,7 +324,7 @@ class TestResourceManager:
)
manager.add_resource(resource1)
manager.add_resource(resource2)
resources = manager.get_resources()
resources = await manager.get_resources()
assert len(resources) == 2
values = list(resources.values())
assert resource1 in values
@ -326,7 +334,7 @@ class TestResourceManager:
class TestResourceTags:
"""Test functionality related to resource tags."""
def test_add_resource_with_tags(self, temp_file: Path):
async def test_add_resource_with_tags(self, temp_file: Path):
"""Test adding a resource with tags."""
manager = ResourceManager()
resource = FileResource(
@ -338,11 +346,12 @@ class TestResourceTags:
manager.add_resource(resource)
# Check that tags are preserved
resources = list(manager.get_resources().values())
resources_dict = await manager.get_resources()
resources = list(resources_dict.values())
assert len(resources) == 1
assert resources[0].tags == {"weather", "data"}
def test_add_function_resource_with_tags(self):
async def test_add_function_resource_with_tags(self):
"""Test adding a function resource with tags."""
manager = ResourceManager()
@ -359,11 +368,12 @@ class TestResourceTags:
)
manager.add_resource(resource)
resources = list(manager.get_resources().values())
resources_dict = await manager.get_resources()
resources = list(resources_dict.values())
assert len(resources) == 1
assert resources[0].tags == {"sample", "test", "data"}
def test_add_template_with_tags(self):
async def test_add_template_with_tags(self):
"""Test adding a resource template with tags."""
manager = ResourceManager()
@ -379,11 +389,12 @@ class TestResourceTags:
)
manager.add_template(template)
templates = list(manager.get_resource_templates().values())
templates_dict = await manager.get_resource_templates()
templates = list(templates_dict.values())
assert len(templates) == 1
assert templates[0].tags == {"users", "template", "data"}
def test_filter_resources_by_tags(self, temp_file: Path):
async def test_filter_resources_by_tags(self, temp_file: Path):
"""Test filtering resources by tags."""
manager = ResourceManager()
@ -392,7 +403,7 @@ class TestResourceTags:
uri=FileUrl("file://weather-data"),
name="weather_data",
path=temp_file,
tags={"weather", "external"},
tags={"weather", "data"},
)
async def get_user_data():
@ -401,8 +412,10 @@ class TestResourceTags:
resource2 = FunctionResource(
uri=AnyUrl("data://users"),
name="user_data",
description="User data resource",
mime_type="text/plain",
fn=get_user_data,
tags={"users", "internal"},
tags={"users", "data"},
)
async def get_system_data():
@ -411,26 +424,25 @@ class TestResourceTags:
resource3 = FunctionResource(
uri=AnyUrl("data://system"),
name="system_data",
description="System data resource",
mime_type="text/plain",
fn=get_system_data,
tags={"system", "internal"},
tags={"system", "admin"},
)
manager.add_resource(resource1)
manager.add_resource(resource2)
manager.add_resource(resource3)
# Filter resources by tags
internal_resources = [
r for r in manager.get_resources().values() if "internal" in r.tags
]
assert len(internal_resources) == 2
assert {r.name for r in internal_resources} == {"user_data", "system_data"}
# Filter by tags
resources_dict = await manager.get_resources()
data_resources = [r for r in resources_dict.values() if "data" in r.tags]
assert len(data_resources) == 2
assert {r.name for r in data_resources} == {"weather_data", "user_data"}
external_resources = [
r for r in manager.get_resources().values() if "external" in r.tags
]
assert len(external_resources) == 1
assert external_resources[0].name == "weather_data"
admin_resources = [r for r in resources_dict.values() if "admin" in r.tags]
assert len(admin_resources) == 1
assert admin_resources[0].name == "system_data"
class TestCustomResourceKeys:

View file

@ -405,6 +405,7 @@ class TestMatchUriTemplate:
("test://a/b/c", None),
("test://a/x/b", {"x": "x"}),
("test://a/x/y/b", None),
("test://a/1-2/b", {"x": "1-2"}),
],
)
def test_match_uri_template_single_param(

View file

@ -136,7 +136,7 @@ def api_client(fastapi_app: FastAPI) -> AsyncClient:
@pytest.fixture
async def fastmcp_openapi_server_with_all_types(
async def fastmcp_openapi_server(
fastapi_app: FastAPI, api_client: httpx.AsyncClient
) -> FastMCPOpenAPI:
openapi_spec = fastapi_app.openapi()
@ -213,13 +213,11 @@ class TestTools:
assert len(await server.get_resources()) == 0
assert len(await server.get_resource_templates()) == 0
async def test_list_tools(
self, fastmcp_openapi_server_with_all_types: FastMCPOpenAPI
):
async def test_list_tools(self, fastmcp_openapi_server: FastMCPOpenAPI):
"""
By default, tools exclude GET methods
"""
async with Client(fastmcp_openapi_server_with_all_types) as client:
async with Client(fastmcp_openapi_server) as client:
tools = await client.list_tools()
assert len(tools) == 2
@ -254,13 +252,13 @@ class TestTools:
async def test_call_create_user_tool(
self,
fastmcp_openapi_server_with_all_types: FastMCPOpenAPI,
fastmcp_openapi_server: FastMCPOpenAPI,
api_client,
):
"""
The tool created by the OpenAPI server should be the same as the original
"""
async with Client(fastmcp_openapi_server_with_all_types) as client:
async with Client(fastmcp_openapi_server) as client:
tool_response = await client.call_tool(
"create_user_users_post", {"name": "David", "active": False}
)
@ -274,7 +272,7 @@ class TestTools:
assert len(response.json()) == 4
# Check that the user was created via MCP
async with Client(fastmcp_openapi_server_with_all_types) as client:
async with Client(fastmcp_openapi_server) as client:
user_response = await client.read_resource("resource://get_user_users/4")
response_text = user_response[0].text # type: ignore[attr-defined]
user = json.loads(response_text)
@ -282,13 +280,13 @@ class TestTools:
async def test_call_update_user_name_tool(
self,
fastmcp_openapi_server_with_all_types: FastMCPOpenAPI,
fastmcp_openapi_server: FastMCPOpenAPI,
api_client,
):
"""
The tool created by the OpenAPI server should be the same as the original
"""
async with Client(fastmcp_openapi_server_with_all_types) as client:
async with Client(fastmcp_openapi_server) as client:
tool_response = await client.call_tool(
"update_user_name_users",
{"user_id": 1, "name": "XYZ"},
@ -303,7 +301,7 @@ class TestTools:
assert expected_data in response.json()
# Check that the user was updated via MCP
async with Client(fastmcp_openapi_server_with_all_types) as client:
async with Client(fastmcp_openapi_server) as client:
user_response = await client.read_resource("resource://get_user_users/1")
response_text = user_response[0].text # type: ignore[attr-defined]
user = json.loads(response_text)
@ -335,13 +333,11 @@ class TestTools:
class TestResources:
async def test_list_resources(
self, fastmcp_openapi_server_with_all_types: FastMCPOpenAPI
):
async def test_list_resources(self, fastmcp_openapi_server: FastMCPOpenAPI):
"""
By default, resources exclude GET methods without parameters
"""
async with Client(fastmcp_openapi_server_with_all_types) as client:
async with Client(fastmcp_openapi_server) as client:
resources = await client.list_resources()
assert len(resources) == 4
assert resources[0].uri == AnyUrl("resource://get_users_users_get")
@ -349,7 +345,7 @@ class TestResources:
async def test_get_resource(
self,
fastmcp_openapi_server_with_all_types: FastMCPOpenAPI,
fastmcp_openapi_server: FastMCPOpenAPI,
api_client,
users_db: dict[int, User],
):
@ -360,7 +356,7 @@ class TestResources:
json_users = TypeAdapter(list[User]).dump_python(
sorted(users_db.values(), key=lambda x: x.id)
)
async with Client(fastmcp_openapi_server_with_all_types) as client:
async with Client(fastmcp_openapi_server) as client:
resource_response = await client.read_resource(
"resource://get_users_users_get"
)
@ -372,11 +368,11 @@ class TestResources:
async def test_get_bytes_resource(
self,
fastmcp_openapi_server_with_all_types: FastMCPOpenAPI,
fastmcp_openapi_server: FastMCPOpenAPI,
api_client,
):
"""Test reading a resource that returns bytes."""
async with Client(fastmcp_openapi_server_with_all_types) as client:
async with Client(fastmcp_openapi_server) as client:
resource_response = await client.read_resource(
"resource://ping_bytes_ping_bytes_get"
)
@ -385,23 +381,23 @@ class TestResources:
async def test_get_str_resource(
self,
fastmcp_openapi_server_with_all_types: FastMCPOpenAPI,
fastmcp_openapi_server: FastMCPOpenAPI,
api_client,
):
"""Test reading a resource that returns a string."""
async with Client(fastmcp_openapi_server_with_all_types) as client:
async with Client(fastmcp_openapi_server) as client:
resource_response = await client.read_resource("resource://ping_ping_get")
assert resource_response[0].text == "pong" # type: ignore[attr-defined]
class TestResourceTemplates:
async def test_list_resource_templates(
self, fastmcp_openapi_server_with_all_types: FastMCPOpenAPI
self, fastmcp_openapi_server: FastMCPOpenAPI
):
"""
By default, resource templates exclude GET methods without parameters
"""
async with Client(fastmcp_openapi_server_with_all_types) as client:
async with Client(fastmcp_openapi_server) as client:
resource_templates = await client.list_resource_templates()
assert len(resource_templates) == 2
assert resource_templates[0].name == "get_user_users"
@ -416,7 +412,7 @@ class TestResourceTemplates:
async def test_get_resource_template(
self,
fastmcp_openapi_server_with_all_types: FastMCPOpenAPI,
fastmcp_openapi_server: FastMCPOpenAPI,
api_client,
users_db: dict[int, User],
):
@ -424,7 +420,7 @@ class TestResourceTemplates:
The resource template created by the OpenAPI server should be the same as the original
"""
user_id = 2
async with Client(fastmcp_openapi_server_with_all_types) as client:
async with Client(fastmcp_openapi_server) as client:
resource_response = await client.read_resource(
f"resource://get_user_users/{user_id}"
)
@ -437,7 +433,7 @@ class TestResourceTemplates:
async def test_get_resource_template_multi_param(
self,
fastmcp_openapi_server_with_all_types: FastMCPOpenAPI,
fastmcp_openapi_server: FastMCPOpenAPI,
api_client,
users_db: dict[int, User],
):
@ -446,7 +442,7 @@ class TestResourceTemplates:
"""
user_id = 2
is_active = True
async with Client(fastmcp_openapi_server_with_all_types) as client:
async with Client(fastmcp_openapi_server) as client:
resource_response = await client.read_resource(
f"resource://get_user_active_state_users/{is_active}/{user_id}"
)
@ -459,13 +455,11 @@ class TestResourceTemplates:
class TestPrompts:
async def test_list_prompts(
self, fastmcp_openapi_server_with_all_types: FastMCPOpenAPI
):
async def test_list_prompts(self, fastmcp_openapi_server: FastMCPOpenAPI):
"""
By default, there are no prompts.
"""
async with Client(fastmcp_openapi_server_with_all_types) as client:
async with Client(fastmcp_openapi_server) as client:
prompts = await client.list_prompts()
assert len(prompts) == 0
@ -474,11 +468,11 @@ class TestTagTransfer:
"""Tests for transferring tags from OpenAPI routes to MCP objects."""
async def test_tags_transferred_to_tools(
self, fastmcp_openapi_server_with_all_types: FastMCPOpenAPI
self, fastmcp_openapi_server: FastMCPOpenAPI
):
"""Test that tags from OpenAPI routes are correctly transferred to Tools."""
# Get internal tools directly (not the public API which returns MCP.Content)
tools = fastmcp_openapi_server_with_all_types._tool_manager._list_tools()
tools = await fastmcp_openapi_server._tool_manager._list_tools()
# Find the create_user and update_user_name tools
create_user_tool = next(
@ -502,13 +496,12 @@ class TestTagTransfer:
assert len(update_user_tool.tags) == 2
async def test_tags_transferred_to_resources(
self, fastmcp_openapi_server_with_all_types: FastMCPOpenAPI
self, fastmcp_openapi_server: FastMCPOpenAPI
):
"""Test that tags from OpenAPI routes are correctly transferred to Resources."""
# Get internal resources directly
resources = list(
fastmcp_openapi_server_with_all_types._resource_manager.get_resources().values()
)
resources_dict = await fastmcp_openapi_server._resource_manager.get_resources()
resources = list(resources_dict.values())
# Find the get_users resource
get_users_resource = next(
@ -523,13 +516,14 @@ class TestTagTransfer:
assert len(get_users_resource.tags) == 2
async def test_tags_transferred_to_resource_templates(
self, fastmcp_openapi_server_with_all_types: FastMCPOpenAPI
self, fastmcp_openapi_server: FastMCPOpenAPI
):
"""Test that tags from OpenAPI routes are correctly transferred to ResourceTemplates."""
# Get internal resource templates directly
templates = list(
fastmcp_openapi_server_with_all_types._resource_manager.get_resource_templates().values()
templates_dict = (
await fastmcp_openapi_server._resource_manager.get_resource_templates()
)
templates = list(templates_dict.values())
# Find the get_user template
get_user_template = next(
@ -544,13 +538,14 @@ class TestTagTransfer:
assert len(get_user_template.tags) == 2
async def test_tags_preserved_in_resources_created_from_templates(
self, fastmcp_openapi_server_with_all_types: FastMCPOpenAPI
self, fastmcp_openapi_server: FastMCPOpenAPI
):
"""Test that tags are preserved when creating resources from templates."""
# Get internal resource templates directly
templates = list(
fastmcp_openapi_server_with_all_types._resource_manager.get_resource_templates().values()
templates_dict = (
await fastmcp_openapi_server._resource_manager.get_resource_templates()
)
templates = list(templates_dict.values())
# Find the get_user template
get_user_template = next(
@ -1167,7 +1162,7 @@ class TestDescriptionPropagation:
return httpx.AsyncClient(transport=transport, base_url="http://test")
@pytest.fixture
async def simple_server_with_all_types(self, simple_openapi_spec, mock_client):
async def simple_mcp_server(self, simple_openapi_spec, mock_client):
"""Create a FastMCPOpenAPI server with the simple test spec."""
return FastMCPOpenAPI(
openapi_spec=simple_openapi_spec,
@ -1179,11 +1174,11 @@ class TestDescriptionPropagation:
# --- RESOURCE TESTS ---
async def test_resource_includes_route_description(
self, simple_server_with_all_types
self, simple_mcp_server: FastMCP
):
"""Test that a Resource includes the route description."""
resources = list(
simple_server_with_all_types._resource_manager.get_resources().values()
(await simple_mcp_server._resource_manager.get_resources()).values()
)
list_resource = next((r for r in resources if r.name == "listItems"), None)
@ -1193,11 +1188,11 @@ class TestDescriptionPropagation:
)
async def test_resource_includes_response_description(
self, simple_server_with_all_types
self, simple_mcp_server: FastMCP
):
"""Test that a Resource includes the response description."""
resources = list(
simple_server_with_all_types._resource_manager.get_resources().values()
(await simple_mcp_server._resource_manager.get_resources()).values()
)
list_resource = next((r for r in resources if r.name == "listItems"), None)
@ -1207,11 +1202,11 @@ class TestDescriptionPropagation:
)
async def test_resource_includes_response_model_fields(
self, simple_server_with_all_types
self, simple_mcp_server: FastMCP
):
"""Test that a Resource description includes response model field descriptions."""
resources = list(
simple_server_with_all_types._resource_manager.get_resources().values()
(await simple_mcp_server._resource_manager.get_resources()).values()
)
list_resource = next((r for r in resources if r.name == "listItems"), None)
@ -1230,12 +1225,13 @@ class TestDescriptionPropagation:
# --- RESOURCE TEMPLATE TESTS ---
async def test_template_includes_route_description(
self, simple_server_with_all_types
self, simple_mcp_server: FastMCP
):
"""Test that a ResourceTemplate includes the route description."""
templates = list(
simple_server_with_all_types._resource_manager.get_templates().values()
templates_dict = (
await simple_mcp_server._resource_manager.get_resource_templates()
)
templates = list(templates_dict.values())
get_template = next((t for t in templates if t.name == "getItem"), None)
assert get_template is not None, "getItem template wasn't created"
@ -1244,12 +1240,13 @@ class TestDescriptionPropagation:
)
async def test_template_includes_function_docstring(
self, simple_server_with_all_types
self, simple_mcp_server: FastMCP
):
"""Test that a ResourceTemplate includes the function docstring."""
templates = list(
simple_server_with_all_types._resource_manager.get_templates().values()
templates_dict = (
await simple_mcp_server._resource_manager.get_resource_templates()
)
templates = list(templates_dict.values())
get_template = next((t for t in templates if t.name == "getItem"), None)
assert get_template is not None, "getItem template wasn't created"
@ -1258,12 +1255,13 @@ class TestDescriptionPropagation:
)
async def test_template_includes_path_parameter_description(
self, simple_server_with_all_types
self, simple_mcp_server: FastMCP
):
"""Test that a ResourceTemplate includes path parameter descriptions."""
templates = list(
simple_server_with_all_types._resource_manager.get_templates().values()
templates_dict = (
await simple_mcp_server._resource_manager.get_resource_templates()
)
templates = list(templates_dict.values())
get_template = next((t for t in templates if t.name == "getItem"), None)
assert get_template is not None, "getItem template wasn't created"
@ -1272,12 +1270,13 @@ class TestDescriptionPropagation:
)
async def test_template_includes_query_parameter_description(
self, simple_server_with_all_types
self, simple_mcp_server: FastMCP
):
"""Test that a ResourceTemplate includes query parameter descriptions."""
templates = list(
simple_server_with_all_types._resource_manager.get_templates().values()
templates_dict = (
await simple_mcp_server._resource_manager.get_resource_templates()
)
templates = list(templates_dict.values())
get_template = next((t for t in templates if t.name == "getItem"), None)
assert get_template is not None, "getItem template wasn't created"
@ -1286,12 +1285,13 @@ class TestDescriptionPropagation:
)
async def test_template_parameter_schema_includes_description(
self, simple_server_with_all_types
self, simple_mcp_server: FastMCP
):
"""Test that a ResourceTemplate's parameter schema includes parameter descriptions."""
templates = list(
simple_server_with_all_types._resource_manager.get_templates().values()
templates_dict = (
await simple_mcp_server._resource_manager.get_resource_templates()
)
templates = list(templates_dict.values())
get_template = next((t for t in templates if t.name == "getItem"), None)
assert get_template is not None, "getItem template wasn't created"
@ -1311,9 +1311,10 @@ class TestDescriptionPropagation:
# --- TOOL TESTS ---
async def test_tool_includes_route_description(self, simple_server_with_all_types):
async def test_tool_includes_route_description(self, simple_mcp_server: FastMCP):
"""Test that a Tool includes the route description."""
tools = simple_server_with_all_types._tool_manager.list_tools()
tools_dict = await simple_mcp_server._tool_manager.get_tools()
tools = list(tools_dict.values())
create_tool = next((t for t in tools if t.name == "createItem"), None)
assert create_tool is not None, "createItem tool wasn't created"
@ -1321,9 +1322,10 @@ class TestDescriptionPropagation:
"Route description missing from Tool"
)
async def test_tool_includes_function_docstring(self, simple_server_with_all_types):
async def test_tool_includes_function_docstring(self, simple_mcp_server: FastMCP):
"""Test that a Tool includes the function docstring."""
tools = simple_server_with_all_types._tool_manager.list_tools()
tools_dict = await simple_mcp_server._tool_manager.get_tools()
tools = list(tools_dict.values())
create_tool = next((t for t in tools if t.name == "createItem"), None)
assert create_tool is not None, "createItem tool wasn't created"
@ -1333,10 +1335,11 @@ class TestDescriptionPropagation:
)
async def test_tool_parameter_schema_includes_property_description(
self, simple_server_with_all_types
self, simple_mcp_server: FastMCP
):
"""Test that a Tool's parameter schema includes property descriptions from request model."""
tools = simple_server_with_all_types._tool_manager.list_tools()
tools_dict = await simple_mcp_server._tool_manager.get_tools()
tools = list(tools_dict.values())
create_tool = next((t for t in tools if t.name == "createItem"), None)
assert create_tool is not None, "createItem tool wasn't created"
@ -1356,9 +1359,9 @@ class TestDescriptionPropagation:
# --- CLIENT API TESTS ---
async def test_client_api_resource_description(self, simple_server_with_all_types):
async def test_client_api_resource_description(self, simple_mcp_server: FastMCP):
"""Test that Resource descriptions are accessible via the client API."""
async with Client(simple_server_with_all_types) as client:
async with Client(simple_mcp_server) as client:
resources = await client.list_resources()
list_resource = next((r for r in resources if r.name == "listItems"), None)
@ -1370,9 +1373,9 @@ class TestDescriptionPropagation:
"Route description missing in Resource from client API"
)
async def test_client_api_template_description(self, simple_server_with_all_types):
async def test_client_api_template_description(self, simple_mcp_server: FastMCP):
"""Test that ResourceTemplate descriptions are accessible via the client API."""
async with Client(simple_server_with_all_types) as client:
async with Client(simple_mcp_server) as client:
templates = await client.list_resource_templates()
get_template = next((t for t in templates if t.name == "getItem"), None)
@ -1384,9 +1387,9 @@ class TestDescriptionPropagation:
"Route description missing in ResourceTemplate from client API"
)
async def test_client_api_tool_description(self, simple_server_with_all_types):
async def test_client_api_tool_description(self, simple_mcp_server: FastMCP):
"""Test that Tool descriptions are accessible via the client API."""
async with Client(simple_server_with_all_types) as client:
async with Client(simple_mcp_server) as client:
tools = await client.list_tools()
create_tool = next((t for t in tools if t.name == "createItem"), None)
@ -1398,9 +1401,9 @@ class TestDescriptionPropagation:
"Function docstring missing in Tool from client API"
)
async def test_client_api_tool_parameter_schema(self, simple_server_with_all_types):
async def test_client_api_tool_parameter_schema(self, simple_mcp_server: FastMCP):
"""Test that Tool parameter schemas are accessible via the client API."""
async with Client(simple_server_with_all_types) as client:
async with Client(simple_mcp_server) as client:
tools = await client.list_tools()
create_tool = next((t for t in tools if t.name == "createItem"), None)
@ -1533,22 +1536,26 @@ class TestFastAPIDescriptionPropagation:
# Debug: print all components created
print("\nDEBUG - Resources created:")
for name, resource in server._resource_manager.get_resources().items():
resources_dict = await server._resource_manager.get_resources()
for name, resource in resources_dict.items():
print(f" Resource: {name}, Name attribute: {resource.name}")
print("\nDEBUG - Templates created:")
for name, template in server._resource_manager.get_resource_templates().items():
templates_dict = await server._resource_manager.get_resource_templates()
for name, template in templates_dict.items():
print(f" Template: {name}, Name attribute: {template.name}")
print("\nDEBUG - Tools created:")
for tool in server._tool_manager._list_tools():
tools = await server._tool_manager._list_tools()
for tool in tools:
print(f" Tool: {tool.name}")
return server
async def test_resource_includes_function_docstring(self, fastapi_server):
async def test_resource_includes_function_docstring(self, fastapi_server: FastMCP):
"""Test that a Resource includes the function docstring."""
resources = list(fastapi_server._resource_manager.get_resources().values())
resources_dict = await fastapi_server._resource_manager.get_resources()
resources = list(resources_dict.values())
# Now checking for the get_items operation ID rather than list_items
list_resource = next((r for r in resources if "items_get" in r.name), None)
@ -1559,13 +1566,16 @@ class TestFastAPIDescriptionPropagation:
"Function docstring missing from Resource"
)
async def test_resource_includes_response_model_fields(self, fastapi_server):
async def test_resource_includes_response_model_fields(
self, fastapi_server: FastMCP
):
"""Test that a Resource description includes basic response information.
Note: FastAPI doesn't reliably include Pydantic field descriptions in the OpenAPI schema,
so we can only check for basic response information being present.
"""
resources = list(fastapi_server._resource_manager.get_resources().values())
resources_dict = await fastapi_server._resource_manager.get_resources()
resources = list(resources_dict.values())
list_resource = next((r for r in resources if "items_get" in r.name), None)
assert list_resource is not None, "GET /items resource wasn't created"
@ -1579,9 +1589,10 @@ class TestFastAPIDescriptionPropagation:
# We've already verified in TestDescriptionPropagation that when descriptions
# are present in the OpenAPI schema, they are properly included in the component description
async def test_template_includes_function_docstring(self, fastapi_server):
async def test_template_includes_function_docstring(self, fastapi_server: FastMCP):
"""Test that a ResourceTemplate includes the function docstring."""
templates = list(fastapi_server._resource_manager.get_templates().values())
templates_dict = await fastapi_server._resource_manager.get_resource_templates()
templates = list(templates_dict.values())
get_template = next((t for t in templates if "get_item_items" in t.name), None)
assert get_template is not None, "GET /items/{item_id} template wasn't created"
@ -1590,13 +1601,16 @@ class TestFastAPIDescriptionPropagation:
"Function docstring missing from ResourceTemplate"
)
async def test_template_includes_path_parameter_description(self, fastapi_server):
async def test_template_includes_path_parameter_description(
self, fastapi_server: FastMCP
):
"""Test that a ResourceTemplate includes path parameter descriptions.
Note: Currently, FastAPI parameter descriptions using Annotated[type, Field(description=...)]
are not properly propagated to the OpenAPI schema. The parameters appear but without the description.
"""
templates = list(fastapi_server._resource_manager.get_templates().values())
templates_dict = await fastapi_server._resource_manager.get_resource_templates()
templates = list(templates_dict.values())
get_template = next((t for t in templates if "get_item_items" in t.name), None)
assert get_template is not None, "GET /items/{item_id} template wasn't created"
@ -1610,13 +1624,16 @@ class TestFastAPIDescriptionPropagation:
"item_id parameter missing from ResourceTemplate description"
)
async def test_template_includes_query_parameter_description(self, fastapi_server):
async def test_template_includes_query_parameter_description(
self, fastapi_server: FastMCP
):
"""Test that a ResourceTemplate includes query parameter descriptions.
Note: Currently, FastAPI parameter descriptions using Annotated[type, Field(description=...)]
are not properly propagated to the OpenAPI schema. The parameters appear but without the description.
"""
templates = list(fastapi_server._resource_manager.get_templates().values())
templates_dict = await fastapi_server._resource_manager.get_resource_templates()
templates = list(templates_dict.values())
get_template = next((t for t in templates if "get_item_items" in t.name), None)
assert get_template is not None, "GET /items/{item_id} template wasn't created"
@ -1630,9 +1647,12 @@ class TestFastAPIDescriptionPropagation:
"fields parameter missing from ResourceTemplate description"
)
async def test_template_parameter_schema_includes_description(self, fastapi_server):
async def test_template_parameter_schema_includes_description(
self, fastapi_server: FastMCP
):
"""Test that a ResourceTemplate's parameter schema includes parameter descriptions."""
templates = list(fastapi_server._resource_manager.get_templates().values())
templates_dict = await fastapi_server._resource_manager.get_resource_templates()
templates = list(templates_dict.values())
get_template = next((t for t in templates if "get_item_items" in t.name), None)
assert get_template is not None, "GET /items/{item_id} template wasn't created"
@ -1650,9 +1670,10 @@ class TestFastAPIDescriptionPropagation:
in get_template.parameters["properties"]["item_id"]["description"]
), "Path parameter description incorrect in schema"
async def test_tool_includes_function_docstring(self, fastapi_server):
async def test_tool_includes_function_docstring(self, fastapi_server: FastMCP):
"""Test that a Tool includes the function docstring."""
tools = fastapi_server._tool_manager.list_tools()
tools_dict = await fastapi_server._tool_manager.get_tools()
tools = list(tools_dict.values())
create_tool = next(
(t for t in tools if "create_item_items_post" == t.name), None
)
@ -1664,7 +1685,7 @@ class TestFastAPIDescriptionPropagation:
)
async def test_tool_parameter_schema_includes_property_description(
self, fastapi_server
self, fastapi_server: FastMCP
):
"""Test that a Tool's parameter schema includes property descriptions from request model.
@ -1672,7 +1693,8 @@ class TestFastAPIDescriptionPropagation:
may not be consistently propagated into the FastAPI OpenAPI schema and thus not into the tool's
parameter schema.
"""
tools = fastapi_server._tool_manager.list_tools()
tools_dict = await fastapi_server._tool_manager.get_tools()
tools = list(tools_dict.values())
create_tool = next(
(t for t in tools if "create_item_items_post" == t.name), None
)
@ -1686,7 +1708,7 @@ class TestFastAPIDescriptionPropagation:
)
# We don't test for the description field content as it may not be consistently propagated
async def test_client_api_resource_description(self, fastapi_server):
async def test_client_api_resource_description(self, fastapi_server: FastMCP):
"""Test that Resource descriptions are accessible via the client API."""
async with Client(fastapi_server) as client:
resources = await client.list_resources()
@ -1700,7 +1722,7 @@ class TestFastAPIDescriptionPropagation:
"Function docstring missing in Resource from client API"
)
async def test_client_api_template_description(self, fastapi_server):
async def test_client_api_template_description(self, fastapi_server: FastMCP):
"""Test that ResourceTemplate descriptions are accessible via the client API."""
async with Client(fastapi_server) as client:
templates = await client.list_resource_templates()
@ -1716,7 +1738,7 @@ class TestFastAPIDescriptionPropagation:
"Function docstring missing in ResourceTemplate from client API"
)
async def test_client_api_tool_description(self, fastapi_server):
async def test_client_api_tool_description(self, fastapi_server: FastMCP):
"""Test that Tool descriptions are accessible via the client API."""
async with Client(fastapi_server) as client:
tools = await client.list_tools()
@ -1732,7 +1754,7 @@ class TestFastAPIDescriptionPropagation:
"Function docstring missing in Tool from client API"
)
async def test_client_api_tool_parameter_schema(self, fastapi_server):
async def test_client_api_tool_parameter_schema(self, fastapi_server: FastMCP):
"""Test that Tool parameter schemas are accessible via the client API."""
async with Client(fastapi_server) as client:
tools = await client.list_tools()
@ -1755,11 +1777,9 @@ class TestFastAPIDescriptionPropagation:
class TestReprMethods:
"""Tests for the custom __repr__ methods of OpenAPI objects."""
async def test_openapi_tool_repr(
self, fastmcp_openapi_server_with_all_types: FastMCPOpenAPI
):
async def test_openapi_tool_repr(self, fastmcp_openapi_server: FastMCPOpenAPI):
"""Test that OpenAPITool's __repr__ method works without recursion errors."""
tools = fastmcp_openapi_server_with_all_types._tool_manager._list_tools()
tools = await fastmcp_openapi_server._tool_manager._list_tools()
tool = next(iter(tools))
# Verify repr doesn't cause recursion and contains expected elements
@ -1769,13 +1789,10 @@ class TestReprMethods:
assert "method=" in tool_repr
assert "path=" in tool_repr
async def test_openapi_resource_repr(
self, fastmcp_openapi_server_with_all_types: FastMCPOpenAPI
):
async def test_openapi_resource_repr(self, fastmcp_openapi_server: FastMCPOpenAPI):
"""Test that OpenAPIResource's __repr__ method works without recursion errors."""
resources = list(
fastmcp_openapi_server_with_all_types._resource_manager.get_resources().values()
)
resources_dict = await fastmcp_openapi_server._resource_manager.get_resources()
resources = list(resources_dict.values())
resource = next(iter(resources))
# Verify repr doesn't cause recursion and contains expected elements
@ -1786,12 +1803,13 @@ class TestReprMethods:
assert "path=" in resource_repr
async def test_openapi_resource_template_repr(
self, fastmcp_openapi_server_with_all_types: FastMCPOpenAPI
self, fastmcp_openapi_server: FastMCPOpenAPI
):
"""Test that OpenAPIResourceTemplate's __repr__ method works without recursion errors."""
templates = list(
fastmcp_openapi_server_with_all_types._resource_manager.get_resource_templates().values()
templates_dict = (
await fastmcp_openapi_server._resource_manager.get_resource_templates()
)
templates = list(templates_dict.values())
template = next(iter(templates))
# Verify repr doesn't cause recursion and contains expected elements
@ -1836,7 +1854,7 @@ class TestEnumHandling:
)
# Get the tools from the server
tools = server._tool_manager._list_tools()
tools = await server._tool_manager._list_tools()
# Find the read_item tool
read_item_tool = next((t for t in tools if t.name == "read_item_items"), None)
@ -1929,7 +1947,7 @@ class TestRouteMapWildcard:
)
# All operations should be mapped to tools
tools = mcp._tool_manager._list_tools()
tools = await mcp._tool_manager._list_tools()
tool_names = {tool.name for tool in tools}
# Check that all 4 operations became tools
@ -2007,11 +2025,11 @@ class TestRouteMapTags:
)
# Check that admin-tagged routes are tools
tools = server._tool_manager.get_tools()
tool_names = {t.name for t in tools.values()}
tools_dict = await server._tool_manager.get_tools()
tool_names = {t.name for t in tools_dict.values()}
resources = server._resource_manager.get_resources()
resource_names = {r.name for r in resources.values()}
resources_dict = await server._resource_manager.get_resources()
resource_names = {r.name for r in resources_dict.values()}
# Routes with "admin" tag should be tools
assert "createUser" in tool_names
@ -2040,11 +2058,11 @@ class TestRouteMapTags:
)
# Check that internal-tagged routes are excluded
resources = server._resource_manager.get_resources()
resource_names = {r.name for r in resources.values()}
resources_dict = await server._resource_manager.get_resources()
resource_names = {r.name for r in resources_dict.values()}
tools = server._tool_manager.get_tools()
tool_names = {t.name for t in tools.values()}
tools_dict = await server._tool_manager.get_tools()
tool_names = {t.name for t in tools_dict.values()}
# Internal-tagged route should be excluded
assert "getAdminStats" not in resource_names
@ -2075,11 +2093,11 @@ class TestRouteMapTags:
route_maps=route_maps,
)
tools = server._tool_manager.get_tools()
tool_names = {t.name for t in tools.values()}
tools_dict = await server._tool_manager.get_tools()
tool_names = {t.name for t in tools_dict.values()}
resources = server._resource_manager.get_resources()
resource_names = {r.name for r in resources.values()}
resources_dict = await server._resource_manager.get_resources()
resource_names = {r.name for r in resources_dict.values()}
# Only createUser has both "users" AND "admin" tags
assert "createUser" in tool_names
@ -2110,11 +2128,11 @@ class TestRouteMapTags:
route_maps=route_maps,
)
tools = server._tool_manager.get_tools()
tool_names = {t.name for t in tools.values()}
tools_dict = await server._tool_manager.get_tools()
tool_names = {t.name for t in tools_dict.values()}
resources = server._resource_manager.get_resources()
resource_names = {r.name for r in resources.values()}
resources_dict = await server._resource_manager.get_resources()
resource_names = {r.name for r in resources_dict.values()}
# Only getAdminStats matches both /admin/ pattern AND "admin" tag
assert "getAdminStats" in tool_names
@ -2140,8 +2158,8 @@ class TestRouteMapTags:
route_maps=route_maps,
)
tools = server._tool_manager.get_tools()
tool_names = {t.name for t in tools.values()}
tools_dict = await server._tool_manager.get_tools()
tool_names = {t.name for t in tools_dict.values()}
# All routes should be tools since empty tags matches everything
expected_tools = {
@ -2246,18 +2264,18 @@ class TestMCPNames:
)
# Check tools use custom names
tools = server._tool_manager._list_tools()
tools = await server._tool_manager._list_tools()
tool_names = {tool.name for tool in tools}
assert "admin_create_user" in tool_names
# Check resource templates use custom names
templates = list(server._resource_manager.get_resource_templates().values())
template_names = {template.name for template in templates}
templates_dict = await server._resource_manager.get_resource_templates()
template_names = {template.name for template in templates_dict.values()}
assert "user_detail" in template_names
# Check resources use custom names
resources = list(server._resource_manager.get_resources().values())
resource_names = {resource.name for resource in resources}
resources_dict = await server._resource_manager.get_resources()
resource_names = {resource.name for resource in resources_dict.values()}
assert "user_list" in resource_names
async def test_mcp_names_fallback_to_operation_id_short(
@ -2276,14 +2294,14 @@ class TestMCPNames:
route_maps=GET_ROUTE_MAPS,
)
tools = server._tool_manager._list_tools()
tools = await server._tool_manager._list_tools()
tool_names = {tool.name for tool in tools}
templates = list(server._resource_manager.get_resource_templates().values())
template_names = {template.name for template in templates}
templates_dict = await server._resource_manager.get_resource_templates()
template_names = {template.name for template in templates_dict.values()}
resources = list(server._resource_manager.get_resources().values())
resource_names = {resource.name for resource in resources}
resources_dict = await server._resource_manager.get_resources()
resource_names = {resource.name for resource in resources_dict.values()}
# Custom mapped name should be used
assert "custom_user_list" in resource_names
@ -2300,9 +2318,11 @@ class TestMCPNames:
route_maps=GET_ROUTE_MAPS,
)
resources = list(server._resource_manager.get_resources().values())
resources_dict = await server._resource_manager.get_resources()
resource_names = {
resource.name for resource in resources if resource.name is not None
resource.name
for resource in resources_dict.values()
if resource.name is not None
}
# Special chars and spaces should be slugified
@ -2330,14 +2350,14 @@ class TestMCPNames:
# Check all component types
all_names = []
tools = server._tool_manager._list_tools()
tools = await server._tool_manager._list_tools()
all_names.extend(tool.name for tool in tools)
resources = list(server._resource_manager.get_resources().values())
all_names.extend(resource.name for resource in resources)
resources_dict = await server._resource_manager.get_resources()
all_names.extend(resource.name for resource in resources_dict.values())
templates = list(server._resource_manager.get_resource_templates().values())
all_names.extend(template.name for template in templates)
templates_dict = await server._resource_manager.get_resource_templates()
all_names.extend(template.name for template in templates_dict.values())
# All names should be 56 characters or less
for name in all_names:
@ -2363,7 +2383,7 @@ class TestMCPNames:
mcp_names=mcp_names,
)
tools = server._tool_manager._list_tools()
tools = await server._tool_manager._list_tools()
tool_names = {tool.name for tool in tools}
assert "openapi_user_list" in tool_names
@ -2395,7 +2415,7 @@ class TestMCPNames:
mcp_names=mcp_names,
)
tools = server._tool_manager._list_tools()
tools = await server._tool_manager._list_tools()
tool_names = {tool.name for tool in tools}
assert "fastapi_create_user" in tool_names
@ -2419,9 +2439,11 @@ class TestMCPNames:
route_maps=GET_ROUTE_MAPS,
)
resources = list(server._resource_manager.get_resources().values())
resources_dict = await server._resource_manager.get_resources()
resource_names = {
resource.name for resource in resources if resource.name is not None
resource.name
for resource in resources_dict.values()
if resource.name is not None
}
# Find the resource that should have the custom name
@ -2496,7 +2518,7 @@ class TestRouteMapMCPTags:
)
# Get the POST tool
tools = server._tool_manager._list_tools()
tools = await server._tool_manager._list_tools()
create_user_tool = next((t for t in tools if "create_user" in t.name), None)
assert create_user_tool is not None, "create_user tool not found"
@ -2530,7 +2552,8 @@ class TestRouteMapMCPTags:
)
# Get the resource
resources = list(server._resource_manager.get_resources().values())
resources_dict = await server._resource_manager.get_resources()
resources = list(resources_dict.values())
get_users_resource = next((r for r in resources if "get_users" in r.name), None)
assert get_users_resource is not None, "get_users resource not found"
@ -2564,7 +2587,8 @@ class TestRouteMapMCPTags:
)
# Get the resource template
templates = list(server._resource_manager.get_resource_templates().values())
templates_dict = await server._resource_manager.get_resource_templates()
templates = list(templates_dict.values())
get_user_template = next((t for t in templates if "get_user" in t.name), None)
assert get_user_template is not None, "get_user template not found"
@ -2610,21 +2634,23 @@ class TestRouteMapMCPTags:
)
# Check tool tags
tools = server._tool_manager._list_tools()
tools = await server._tool_manager._list_tools()
create_tool = next((t for t in tools if "create_user" in t.name), None)
assert create_tool is not None
assert "write-operation" in create_tool.tags
assert "mutation" in create_tool.tags
# Check resource template tags
templates = list(server._resource_manager.get_resource_templates().values())
templates_dict = await server._resource_manager.get_resource_templates()
templates = list(templates_dict.values())
detail_template = next((t for t in templates if "get_user" in t.name), None)
assert detail_template is not None
assert "detail" in detail_template.tags
assert "single-item" in detail_template.tags
# Check resource tags
resources = list(server._resource_manager.get_resources().values())
resources_dict = await server._resource_manager.get_resources()
resources = list(resources_dict.values())
list_resource = next((r for r in resources if "get_users" in r.name), None)
assert list_resource is not None
assert "list" in list_resource.tags

View file

@ -71,7 +71,8 @@ class TestBasicMount:
# Mount without deprecated parameters
main_app.mount(api_app, "api")
async def test_mount_with_no_prefix(self):
@pytest.mark.parametrize("prefix", ["", None])
async def test_mount_with_no_prefix(self, prefix):
main_app = FastMCP("MainApp")
sub_app = FastMCP("SubApp")
@ -80,7 +81,7 @@ class TestBasicMount:
return "This is from the sub app"
# Mount with empty prefix but without deprecated separators
main_app.mount(sub_app, prefix="")
main_app.mount(sub_app, prefix=prefix)
tools = await main_app.get_tools()
# With empty prefix, the tool should keep its original name

View file

@ -56,8 +56,8 @@ async def test_resource_prefix_format_in_import_server():
await main_server_protocol.import_server(server, "sub")
# Check that the resources are prefixed correctly
path_resources = main_server_path._resource_manager.get_resources()
protocol_resources = main_server_protocol._resource_manager.get_resources()
path_resources = await main_server_path._resource_manager.get_resources()
protocol_resources = await main_server_protocol._resource_manager.get_resources()
# Path format should be resource://sub/test
assert "resource://sub/test" in path_resources

View file

@ -22,7 +22,8 @@ async def test_tool_annotations_in_tool_manager():
return message
# Check internal tool objects directly
tools = mcp._tool_manager._list_tools()
tools_dict = await mcp._tool_manager.get_tools()
tools = list(tools_dict.values())
assert len(tools) == 1
assert tools[0].annotations is not None
assert tools[0].annotations.title == "Echo Tool"
@ -124,7 +125,8 @@ async def test_direct_tool_annotations_in_tool_manager():
return {"modified": True, **data}
# Check internal tool objects directly
tools = mcp._tool_manager._list_tools()
tools_dict = await mcp._tool_manager.get_tools()
tools = list(tools_dict.values())
assert len(tools) == 1
assert tools[0].annotations is not None
assert tools[0].annotations.title == "Direct Tool"
@ -183,7 +185,8 @@ async def test_add_tool_method_annotations():
mcp.add_tool(tool)
# Check internal tool objects directly
tools = mcp._tool_manager._list_tools()
tools_dict = await mcp._tool_manager.get_tools()
tools = list(tools_dict.values())
assert len(tools) == 1
assert tools[0].annotations is not None
assert tools[0].annotations.title == "Create Item"

View file

@ -19,9 +19,10 @@ async def test_tool_exclude_args_in_tool_manager():
pass
return message
tools = mcp._tool_manager._list_tools()
tools_dict = await mcp._tool_manager.get_tools()
tools = list(tools_dict.values())
assert len(tools) == 1
assert "state" not in echo.parameters["properties"]
assert "state" not in tools[0].parameters["properties"]
async def test_tool_exclude_args_without_default_value_raises_error():
@ -60,7 +61,8 @@ async def test_add_tool_method_exclude_args():
mcp.add_tool(tool)
# Check internal tool objects directly
tools = mcp._tool_manager._list_tools()
tools_dict = await mcp._tool_manager.get_tools()
tools = list(tools_dict.values())
assert len(tools) == 1
assert "state" not in tools[0].parameters["properties"]