Jeremiah Lowin commited on
Commit
506c09a
·
1 Parent(s): a32223b

Update tests

Browse files
docs/servers/middleware.mdx ADDED
@@ -0,0 +1,510 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: MCP Middleware
3
+ sidebarTitle: Middleware
4
+ description: Add cross-cutting functionality to your MCP server with middleware that can inspect, modify, and respond to all MCP requests and responses.
5
+ icon: layers
6
+ ---
7
+
8
+ import { VersionBadge } from "/snippets/version-badge.mdx"
9
+
10
+ <VersionBadge version="2.8.0" />
11
+
12
+ 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.
13
+
14
+ <Warning>
15
+ 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.
16
+ </Warning>
17
+
18
+ ## What is MCP Middleware?
19
+
20
+ 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.
21
+
22
+ Common use cases for MCP middleware include:
23
+ - **Authentication and Authorization**: Verify client permissions before executing operations
24
+ - **Logging and Monitoring**: Track usage patterns and performance metrics
25
+ - **Rate Limiting**: Control request frequency per client or operation type
26
+ - **Request/Response Transformation**: Modify data before it reaches tools or after it leaves
27
+ - **Caching**: Store frequently requested data to improve performance
28
+ - **Error Handling**: Provide consistent error responses across your server
29
+
30
+ ## How MCP Middleware Works
31
+
32
+ MCP middleware operates on a pipeline model where each middleware can:
33
+
34
+ 1. **Inspect the incoming request** and its context
35
+ 2. **Modify the request** before passing it to the next middleware or handler
36
+ 3. **Execute the next middleware/handler** in the chain
37
+ 4. **Inspect and modify the response** before returning it
38
+ 5. **Handle errors** that occur during processing
39
+
40
+ The middleware system provides specialized hooks for different MCP operations:
41
+
42
+ - `on_message`: Called for all MCP messages (requests and notifications)
43
+ - `on_request`: Called specifically for MCP requests (that expect responses)
44
+ - `on_notification`: Called specifically for MCP notifications (fire-and-forget)
45
+ - `on_call_tool`: Called when tools are being executed
46
+ - `on_read_resource`: Called when resources are being read
47
+ - `on_get_prompt`: Called when prompts are being retrieved
48
+ - `on_list_tools`: Called when listing available tools
49
+ - `on_list_resources`: Called when listing available resources
50
+ - `on_list_resource_templates`: Called when listing resource templates
51
+ - `on_list_prompts`: Called when listing available prompts
52
+
53
+ <Tip>
54
+ 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.
55
+ </Tip>
56
+
57
+ ## Creating Middleware
58
+
59
+ ### Basic Middleware Structure
60
+
61
+ MCP middleware is implemented by subclassing the `MCPMiddleware` base class and overriding the hooks you need:
62
+
63
+ ```python
64
+ from fastmcp import FastMCP
65
+ from fastmcp.server.middleware import MCPMiddleware, MiddlewareContext
66
+
67
+ class LoggingMiddleware(MCPMiddleware):
68
+ """Middleware that logs all MCP operations."""
69
+
70
+ async def on_message(self, context: MiddlewareContext, call_next):
71
+ """Called for all MCP messages."""
72
+ print(f"Processing {context.method} from {context.source}")
73
+
74
+ # Call the next middleware/handler in the chain
75
+ result = await call_next(context)
76
+
77
+ print(f"Completed {context.method}")
78
+ return result
79
+
80
+ async def on_call_tool(self, context: MiddlewareContext, call_next):
81
+ """Called specifically for tool calls."""
82
+ tool_name = context.message.name
83
+ print(f"Calling tool: {tool_name}")
84
+
85
+ result = await call_next(context)
86
+
87
+ print(f"Tool {tool_name} completed")
88
+ return result
89
+
90
+ # Add middleware to your server
91
+ mcp = FastMCP("MyServer")
92
+ mcp.add_middleware(LoggingMiddleware())
93
+ ```
94
+
95
+ ### Middleware Context
96
+
97
+ The `MiddlewareContext` object provides access to information about the current request:
98
+
99
+ ```python
100
+ class InspectionMiddleware(MCPMiddleware):
101
+ async def on_request(self, context: MiddlewareContext, call_next):
102
+ # Access request information
103
+ method = context.method # e.g., "tools/call"
104
+ source = context.source # "client" or "server"
105
+ message_type = context.type # "request" or "notification"
106
+ timestamp = context.timestamp # When the request was received
107
+ message = context.message # The actual MCP message
108
+ fastmcp_context = context.fastmcp_context # FastMCP Context object (if available)
109
+
110
+ # Continue processing
111
+ return await call_next(context)
112
+ ```
113
+
114
+ ### Middleware Hooks
115
+
116
+ Each middleware hook receives a `MiddlewareContext` and a `call_next` function. The hooks are organized in a hierarchy:
117
+
118
+ 1. **`on_message`**: The broadest hook, called for all MCP messages
119
+ 2. **`on_request`** / **`on_notification`**: Called based on message type
120
+ 3. **Operation-specific hooks**: Called for specific MCP operations
121
+
122
+ ```python
123
+ class ComprehensiveMiddleware(MCPMiddleware):
124
+ async def on_message(self, context: MiddlewareContext, call_next):
125
+ """Called for ALL messages (requests and notifications)."""
126
+ print(f"Message: {context.method}")
127
+ return await call_next(context)
128
+
129
+ async def on_request(self, context: MiddlewareContext, call_next):
130
+ """Called only for requests (messages that expect responses)."""
131
+ print(f"Request: {context.method}")
132
+ return await call_next(context)
133
+
134
+ async def on_call_tool(self, context: MiddlewareContext, call_next):
135
+ """Called only for tool execution requests."""
136
+ tool_name = context.message.name
137
+ print(f"Executing tool: {tool_name}")
138
+ return await call_next(context)
139
+ ```
140
+
141
+ ## Middleware Examples
142
+
143
+ ### Authentication Middleware
144
+
145
+ ```python
146
+ from fastmcp.server.middleware import MCPMiddleware, MiddlewareContext
147
+ from fastmcp.exceptions import ToolError
148
+
149
+ class AuthenticationMiddleware(MCPMiddleware):
150
+ def __init__(self, required_token: str):
151
+ self.required_token = required_token
152
+
153
+ async def on_request(self, context: MiddlewareContext, call_next):
154
+ """Verify authentication for all requests."""
155
+
156
+ # Check if this is an HTTP request with headers
157
+ if hasattr(context, 'fastmcp_context') and context.fastmcp_context:
158
+ try:
159
+ # Access HTTP request if available
160
+ request = context.fastmcp_context.get_http_request()
161
+ auth_header = request.headers.get("Authorization")
162
+
163
+ if not auth_header or not auth_header.startswith("Bearer "):
164
+ raise ToolError("Missing or invalid authorization header")
165
+
166
+ token = auth_header.split(" ", 1)[1]
167
+ if token != self.required_token:
168
+ raise ToolError("Invalid authentication token")
169
+
170
+ except Exception:
171
+ # If HTTP request is not available, continue without auth
172
+ pass
173
+
174
+ return await call_next(context)
175
+
176
+ # Usage
177
+ mcp = FastMCP("SecureServer")
178
+ mcp.add_middleware(AuthenticationMiddleware("secret-token-123"))
179
+ ```
180
+
181
+ ### Performance Monitoring Middleware
182
+
183
+ ```python
184
+ import time
185
+ import logging
186
+
187
+ class PerformanceMiddleware(MCPMiddleware):
188
+ def __init__(self):
189
+ self.logger = logging.getLogger("performance")
190
+
191
+ async def on_call_tool(self, context: MiddlewareContext, call_next):
192
+ """Monitor tool execution performance."""
193
+ tool_name = context.message.name
194
+ start_time = time.time()
195
+
196
+ try:
197
+ result = await call_next(context)
198
+ execution_time = time.time() - start_time
199
+
200
+ self.logger.info(
201
+ f"Tool {tool_name} completed in {execution_time:.3f}s"
202
+ )
203
+
204
+ return result
205
+
206
+ except Exception as e:
207
+ execution_time = time.time() - start_time
208
+ self.logger.error(
209
+ f"Tool {tool_name} failed after {execution_time:.3f}s: {e}"
210
+ )
211
+ raise
212
+ ```
213
+
214
+ ### Request/Response Transformation Middleware
215
+
216
+ ```python
217
+ class TransformationMiddleware(MCPMiddleware):
218
+ async def on_call_tool(self, context: MiddlewareContext, call_next):
219
+ """Transform tool arguments and results."""
220
+
221
+ # Access and modify tool arguments
222
+ if hasattr(context.message, 'arguments'):
223
+ args = context.message.arguments or {}
224
+
225
+ # Example: Add a timestamp to all tool calls
226
+ args['_middleware_timestamp'] = context.timestamp.isoformat()
227
+
228
+ # Create a modified context
229
+ modified_context = context.copy(
230
+ message=context.message.model_copy(update={'arguments': args})
231
+ )
232
+ else:
233
+ modified_context = context
234
+
235
+ # Execute with modified context
236
+ result = await call_next(modified_context)
237
+
238
+ # Transform the result if needed
239
+ if hasattr(result, 'content'):
240
+ # Example: Add metadata to tool results
241
+ if result.content and len(result.content) > 0:
242
+ original_content = result.content[0].text
243
+ enhanced_content = f"[Processed at {context.timestamp}]\n{original_content}"
244
+ result.content[0].text = enhanced_content
245
+
246
+ return result
247
+ ```
248
+
249
+ ### Rate Limiting Middleware
250
+
251
+ ```python
252
+ import asyncio
253
+ from collections import defaultdict
254
+ from datetime import datetime, timedelta
255
+
256
+ class RateLimitMiddleware(MCPMiddleware):
257
+ def __init__(self, max_requests: int = 100, window_minutes: int = 1):
258
+ self.max_requests = max_requests
259
+ self.window = timedelta(minutes=window_minutes)
260
+ self.requests = defaultdict(list) # client_id -> [timestamps]
261
+
262
+ async def on_request(self, context: MiddlewareContext, call_next):
263
+ """Implement rate limiting per client."""
264
+
265
+ # Get client identifier (you may need to implement this based on your auth)
266
+ client_id = getattr(context.fastmcp_context, 'client_id', 'anonymous')
267
+
268
+ now = datetime.now()
269
+
270
+ # Clean old requests
271
+ self.requests[client_id] = [
272
+ timestamp for timestamp in self.requests[client_id]
273
+ if now - timestamp < self.window
274
+ ]
275
+
276
+ # Check rate limit
277
+ if len(self.requests[client_id]) >= self.max_requests:
278
+ raise ToolError(
279
+ f"Rate limit exceeded: {self.max_requests} requests per "
280
+ f"{self.window.total_seconds()/60:.0f} minutes"
281
+ )
282
+
283
+ # Record this request
284
+ self.requests[client_id].append(now)
285
+
286
+ return await call_next(context)
287
+ ```
288
+
289
+ ## Adding Middleware to Your Server
290
+
291
+ ### Single Middleware
292
+
293
+ ```python
294
+ from fastmcp import FastMCP
295
+
296
+ mcp = FastMCP("MyServer")
297
+
298
+ # Add a single middleware instance
299
+ logging_middleware = LoggingMiddleware()
300
+ mcp.add_middleware(logging_middleware)
301
+ ```
302
+
303
+ ### Multiple Middleware
304
+
305
+ Middleware is executed in the order it's added to the server:
306
+
307
+ ```python
308
+ mcp = FastMCP("MyServer")
309
+
310
+ # Add multiple middleware - they execute in order
311
+ mcp.add_middleware(AuthenticationMiddleware("secret-token"))
312
+ mcp.add_middleware(PerformanceMiddleware())
313
+ mcp.add_middleware(LoggingMiddleware())
314
+
315
+ # Request flow:
316
+ # 1. AuthenticationMiddleware.on_request()
317
+ # 2. PerformanceMiddleware.on_request()
318
+ # 3. LoggingMiddleware.on_request()
319
+ # 4. Actual tool/resource handler
320
+ # 5. LoggingMiddleware response processing
321
+ # 6. PerformanceMiddleware response processing
322
+ # 7. AuthenticationMiddleware response processing
323
+ ```
324
+
325
+ ## Advanced Patterns
326
+
327
+ ### Conditional Middleware
328
+
329
+ ```python
330
+ class ConditionalMiddleware(MCPMiddleware):
331
+ def __init__(self, condition_func):
332
+ self.should_process = condition_func
333
+
334
+ async def on_call_tool(self, context: MiddlewareContext, call_next):
335
+ """Only process certain tools."""
336
+
337
+ if not self.should_process(context.message.name):
338
+ # Skip processing for this tool
339
+ return await call_next(context)
340
+
341
+ # Apply middleware logic
342
+ print(f"Processing tool: {context.message.name}")
343
+ return await call_next(context)
344
+
345
+ # Usage
346
+ def only_expensive_tools(tool_name: str) -> bool:
347
+ return tool_name in ["complex_analysis", "heavy_computation"]
348
+
349
+ mcp.add_middleware(ConditionalMiddleware(only_expensive_tools))
350
+ ```
351
+
352
+ ### Middleware with State
353
+
354
+ ```python
355
+ class StatefulMiddleware(MCPMiddleware):
356
+ def __init__(self):
357
+ self.call_count = 0
358
+ self.tools_used = set()
359
+
360
+ async def on_call_tool(self, context: MiddlewareContext, call_next):
361
+ """Track usage statistics."""
362
+ self.call_count += 1
363
+ self.tools_used.add(context.message.name)
364
+
365
+ print(f"Total calls: {self.call_count}, Unique tools: {len(self.tools_used)}")
366
+
367
+ return await call_next(context)
368
+
369
+ def get_stats(self):
370
+ return {
371
+ "total_calls": self.call_count,
372
+ "unique_tools": len(self.tools_used),
373
+ "tools_used": list(self.tools_used)
374
+ }
375
+ ```
376
+
377
+ ### Error Handling Middleware
378
+
379
+ ```python
380
+ class ErrorHandlingMiddleware(MCPMiddleware):
381
+ async def on_call_tool(self, context: MiddlewareContext, call_next):
382
+ """Provide consistent error handling."""
383
+ try:
384
+ return await call_next(context)
385
+ except ToolError:
386
+ # Re-raise ToolErrors as-is
387
+ raise
388
+ except Exception as e:
389
+ # Log the error and convert to a user-friendly message
390
+ logging.error(f"Tool {context.message.name} failed: {e}")
391
+ raise ToolError(f"Tool execution failed: {str(e)}")
392
+ ```
393
+
394
+ ## Best Practices
395
+
396
+ ### Performance Considerations
397
+
398
+ 1. **Keep middleware lightweight**: Avoid heavy computations in middleware
399
+ 2. **Use async operations**: Don't block the event loop with synchronous operations
400
+ 3. **Cache when possible**: Store frequently accessed data to avoid repeated lookups
401
+
402
+ ```python
403
+ class EfficientMiddleware(MCPMiddleware):
404
+ def __init__(self):
405
+ self._cache = {}
406
+
407
+ async def on_call_tool(self, context: MiddlewareContext, call_next):
408
+ """Example of efficient middleware with caching."""
409
+
410
+ # Check cache first
411
+ cache_key = f"{context.message.name}:{hash(str(context.message.arguments))}"
412
+
413
+ if cache_key in self._cache:
414
+ print("Returning cached result")
415
+ return self._cache[cache_key]
416
+
417
+ # Execute and cache result
418
+ result = await call_next(context)
419
+ self._cache[cache_key] = result
420
+
421
+ return result
422
+ ```
423
+
424
+ ### Error Handling
425
+
426
+ 1. **Always call `call_next`**: Unless you're intentionally stopping the chain
427
+ 2. **Handle exceptions appropriately**: Don't let middleware errors break the entire request
428
+ 3. **Use `ToolError` for client-facing errors**: Keep internal errors internal
429
+
430
+ ```python
431
+ class RobustMiddleware(MCPMiddleware):
432
+ async def on_request(self, context: MiddlewareContext, call_next):
433
+ """Robust error handling example."""
434
+ try:
435
+ # Middleware logic here
436
+ return await call_next(context)
437
+ except ToolError:
438
+ # Client-facing errors should be re-raised
439
+ raise
440
+ except Exception as e:
441
+ # Log internal errors but don't expose details
442
+ logging.error(f"Middleware error: {e}")
443
+ # Optionally continue without middleware processing
444
+ return await call_next(context)
445
+ ```
446
+
447
+ ### Testing Middleware
448
+
449
+ ```python
450
+ import pytest
451
+ from fastmcp import FastMCP, Client
452
+
453
+ @pytest.mark.asyncio
454
+ async def test_logging_middleware():
455
+ """Test middleware functionality."""
456
+
457
+ # Create server with middleware
458
+ mcp = FastMCP("TestServer")
459
+ logging_middleware = LoggingMiddleware()
460
+ mcp.add_middleware(logging_middleware)
461
+
462
+ @mcp.tool
463
+ def test_tool(x: int) -> int:
464
+ return x * 2
465
+
466
+ # Test with client
467
+ async with Client(mcp) as client:
468
+ result = await client.call_tool("test_tool", {"x": 5})
469
+ assert result == 10
470
+
471
+ # Verify middleware was called
472
+ # (You'll need to add tracking to your middleware for testing)
473
+ ```
474
+
475
+ ## Server Composition and Middleware
476
+
477
+ When using [Server Composition](/servers/composition) with `mount` or `import_server`, middleware behavior follows these rules:
478
+
479
+ 1. **Parent server middleware** runs for all requests, including those routed to mounted servers
480
+ 2. **Mounted server middleware** only runs for requests handled by that specific server
481
+ 3. **Middleware order** is preserved within each server
482
+
483
+ ```python
484
+ # Parent server with middleware
485
+ parent = FastMCP("Parent")
486
+ parent.add_middleware(AuthenticationMiddleware("token"))
487
+
488
+ # Child server with its own middleware
489
+ child = FastMCP("Child")
490
+ child.add_middleware(LoggingMiddleware())
491
+
492
+ @child.tool
493
+ def child_tool() -> str:
494
+ return "from child"
495
+
496
+ # Mount the child server
497
+ parent.mount(child, prefix="child")
498
+
499
+ # Request to "child_tool" will:
500
+ # 1. Run parent's AuthenticationMiddleware
501
+ # 2. Route to child server
502
+ # 3. Run child's LoggingMiddleware
503
+ # 4. Execute child_tool
504
+ ```
505
+
506
+ 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.
507
+
508
+ <Tip>
509
+ 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.
510
+ </Tip>
src/fastmcp/prompts/prompt_manager.py CHANGED
@@ -27,7 +27,7 @@ class PromptManager:
27
  mask_error_details: bool | None = None,
28
  ):
29
  self._prompts: dict[str, Prompt] = {}
30
- self._mounted_sources: list[MountedServer] = []
31
  self.mask_error_details = mask_error_details or settings.mask_error_details
32
 
33
  # Default to "warn" if None is provided
@@ -44,7 +44,7 @@ class PromptManager:
44
 
45
  def mount(self, server: MountedServer) -> None:
46
  """Adds a mounted server as a source for prompts."""
47
- self._mounted_sources.append(server)
48
 
49
  async def _load_prompts(self, *, via_server: bool = False) -> dict[str, Prompt]:
50
  """
@@ -56,7 +56,7 @@ class PromptManager:
56
  """
57
  all_prompts: dict[str, Prompt] = {}
58
 
59
- for mounted in self._mounted_sources:
60
  try:
61
  if via_server:
62
  # Use the server-to-server filtered path
@@ -188,12 +188,16 @@ class PromptManager:
188
  raise PromptError(f"Error rendering prompt {name!r}: {e}") from e
189
 
190
  # 2. Check mounted servers using the filtered protocol path.
191
- for mounted in reversed(self._mounted_sources):
192
- if mounted.prefix and name.startswith(f"{mounted.prefix}_"):
193
- name_on_child = name.removeprefix(f"{mounted.prefix}_")
194
- try:
195
- return await mounted.server._get_prompt(name_on_child, arguments)
196
- except NotFoundError:
197
  continue
 
 
 
 
198
 
199
  raise NotFoundError(f"Unknown prompt: {name}")
 
27
  mask_error_details: bool | None = None,
28
  ):
29
  self._prompts: dict[str, Prompt] = {}
30
+ self._mounted_servers: list[MountedServer] = []
31
  self.mask_error_details = mask_error_details or settings.mask_error_details
32
 
33
  # Default to "warn" if None is provided
 
44
 
45
  def mount(self, server: MountedServer) -> None:
46
  """Adds a mounted server as a source for prompts."""
47
+ self._mounted_servers.append(server)
48
 
49
  async def _load_prompts(self, *, via_server: bool = False) -> dict[str, Prompt]:
50
  """
 
56
  """
57
  all_prompts: dict[str, Prompt] = {}
58
 
59
+ for mounted in self._mounted_servers:
60
  try:
61
  if via_server:
62
  # Use the server-to-server filtered path
 
188
  raise PromptError(f"Error rendering prompt {name!r}: {e}") from e
189
 
190
  # 2. Check mounted servers using the filtered protocol path.
191
+ for mounted in reversed(self._mounted_servers):
192
+ prompt_key = name
193
+ if mounted.prefix:
194
+ if name.startswith(f"{mounted.prefix}_"):
195
+ prompt_key = name.removeprefix(f"{mounted.prefix}_")
196
+ else:
197
  continue
198
+ try:
199
+ return await mounted.server._get_prompt(prompt_key, arguments)
200
+ except NotFoundError:
201
+ continue
202
 
203
  raise NotFoundError(f"Unknown prompt: {name}")
src/fastmcp/resources/resource_manager.py CHANGED
@@ -43,7 +43,7 @@ class ResourceManager:
43
  """
44
  self._resources: dict[str, Resource] = {}
45
  self._templates: dict[str, ResourceTemplate] = {}
46
- self._mounted_sources: list[MountedServer] = []
47
  self.mask_error_details = mask_error_details or settings.mask_error_details
48
 
49
  # Default to "warn" if None is provided
@@ -59,7 +59,7 @@ class ResourceManager:
59
 
60
  def mount(self, server: MountedServer) -> None:
61
  """Adds a mounted server as a source for resources and templates."""
62
- self._mounted_sources.append(server)
63
 
64
  async def get_resources(self) -> dict[str, Resource]:
65
  """Get all registered resources, keyed by URI."""
@@ -79,7 +79,7 @@ class ResourceManager:
79
  """
80
  all_resources: dict[str, Resource] = {}
81
 
82
- for mounted in self._mounted_sources:
83
  try:
84
  if via_server:
85
  # Use the server-to-server filtered path
@@ -129,7 +129,7 @@ class ResourceManager:
129
  """
130
  all_templates: dict[str, ResourceTemplate] = {}
131
 
132
- for mounted in self._mounted_sources:
133
  try:
134
  if via_server:
135
  # Use the server-to-server filtered path
@@ -457,8 +457,8 @@ class ResourceManager:
457
  ) from e
458
 
459
  # 1b. Check local templates if not found in concrete resources
460
- for template in self._templates.values():
461
- if params := match_uri_template(uri_str, template.uri_template):
462
  try:
463
  resource = await template.create_resource(uri_str, params=params)
464
  return await resource.read()
@@ -483,29 +483,28 @@ class ResourceManager:
483
  # 2. Check mounted servers using the filtered protocol path.
484
  from fastmcp.server.server import has_resource_prefix, remove_resource_prefix
485
 
486
- for mounted in reversed(self._mounted_sources):
487
- resource_uri = uri_str
488
  try:
489
  if mounted.prefix:
490
- # If server has a prefix, check if URI matches and strip prefix
491
  if has_resource_prefix(
492
- resource_uri,
493
  mounted.prefix,
494
  mounted.resource_prefix_format,
495
  ):
496
- resource_uri = remove_resource_prefix(
497
- resource_uri,
498
  mounted.prefix,
499
  mounted.resource_prefix_format,
500
  )
501
  else:
502
  continue
503
 
504
- result = await mounted.server._read_resource(resource_uri)
505
- # Extract content from the first ReadResourceContents
506
- if result and len(result) > 0:
507
  return result[0].content
508
- raise NotFoundError(f"Resource {uri_str!r} returned empty content")
 
509
  except NotFoundError:
510
  continue
511
 
 
43
  """
44
  self._resources: dict[str, Resource] = {}
45
  self._templates: dict[str, ResourceTemplate] = {}
46
+ self._mounted_servers: list[MountedServer] = []
47
  self.mask_error_details = mask_error_details or settings.mask_error_details
48
 
49
  # Default to "warn" if None is provided
 
59
 
60
  def mount(self, server: MountedServer) -> None:
61
  """Adds a mounted server as a source for resources and templates."""
62
+ self._mounted_servers.append(server)
63
 
64
  async def get_resources(self) -> dict[str, Resource]:
65
  """Get all registered resources, keyed by URI."""
 
79
  """
80
  all_resources: dict[str, Resource] = {}
81
 
82
+ for mounted in self._mounted_servers:
83
  try:
84
  if via_server:
85
  # Use the server-to-server filtered path
 
129
  """
130
  all_templates: dict[str, ResourceTemplate] = {}
131
 
132
+ for mounted in self._mounted_servers:
133
  try:
134
  if via_server:
135
  # Use the server-to-server filtered path
 
457
  ) from e
458
 
459
  # 1b. Check local templates if not found in concrete resources
460
+ for key, template in self._templates.items():
461
+ if params := match_uri_template(uri_str, key):
462
  try:
463
  resource = await template.create_resource(uri_str, params=params)
464
  return await resource.read()
 
483
  # 2. Check mounted servers using the filtered protocol path.
484
  from fastmcp.server.server import has_resource_prefix, remove_resource_prefix
485
 
486
+ for mounted in reversed(self._mounted_servers):
487
+ key = uri_str
488
  try:
489
  if mounted.prefix:
 
490
  if has_resource_prefix(
491
+ key,
492
  mounted.prefix,
493
  mounted.resource_prefix_format,
494
  ):
495
+ key = remove_resource_prefix(
496
+ key,
497
  mounted.prefix,
498
  mounted.resource_prefix_format,
499
  )
500
  else:
501
  continue
502
 
503
+ try:
504
+ result = await mounted.server._read_resource(key)
 
505
  return result[0].content
506
+ except NotFoundError:
507
+ continue
508
  except NotFoundError:
509
  continue
510
 
src/fastmcp/resources/template.py CHANGED
@@ -62,6 +62,9 @@ class ResourceTemplate(FastMCPComponent):
62
  description="JSON schema for function parameters"
63
  )
64
 
 
 
 
65
  @staticmethod
66
  def from_function(
67
  fn: Callable[..., Any],
 
62
  description="JSON schema for function parameters"
63
  )
64
 
65
+ def __repr__(self) -> str:
66
+ return f"{self.__class__.__name__}(uri_template={self.uri_template!r}, name={self.name!r}, description={self.description!r}, tags={self.tags})"
67
+
68
  @staticmethod
69
  def from_function(
70
  fn: Callable[..., Any],
src/fastmcp/tools/tool_manager.py CHANGED
@@ -28,7 +28,7 @@ class ToolManager:
28
  mask_error_details: bool | None = None,
29
  ):
30
  self._tools: dict[str, Tool] = {}
31
- self._mounted_sources: list[MountedServer] = []
32
  self.mask_error_details = mask_error_details or settings.mask_error_details
33
 
34
  # Default to "warn" if None is provided
@@ -45,7 +45,7 @@ class ToolManager:
45
 
46
  def mount(self, server: MountedServer) -> None:
47
  """Adds a mounted server as a source for tools."""
48
- self._mounted_sources.append(server)
49
 
50
  async def _load_tools(self, *, via_server: bool = False) -> dict[str, Tool]:
51
  """
@@ -57,7 +57,7 @@ class ToolManager:
57
  """
58
  all_tools: dict[str, Tool] = {}
59
 
60
- for mounted in self._mounted_sources:
61
  try:
62
  if via_server:
63
  # Use the server-to-server filtered path
@@ -201,12 +201,16 @@ class ToolManager:
201
  raise ToolError(f"Error calling tool {key!r}: {e}") from e
202
 
203
  # 2. Check mounted servers using the filtered protocol path.
204
- for mounted in reversed(self._mounted_sources):
205
- if mounted.prefix and key.startswith(f"{mounted.prefix}_"):
206
- key_on_child = key.removeprefix(f"{mounted.prefix}_")
207
- try:
208
- return await mounted.server._call_tool(key_on_child, arguments)
209
- except NotFoundError:
210
  continue
 
 
 
 
211
 
212
  raise NotFoundError(f"Tool {key!r} not found.")
 
28
  mask_error_details: bool | None = None,
29
  ):
30
  self._tools: dict[str, Tool] = {}
31
+ self._mounted_servers: list[MountedServer] = []
32
  self.mask_error_details = mask_error_details or settings.mask_error_details
33
 
34
  # Default to "warn" if None is provided
 
45
 
46
  def mount(self, server: MountedServer) -> None:
47
  """Adds a mounted server as a source for tools."""
48
+ self._mounted_servers.append(server)
49
 
50
  async def _load_tools(self, *, via_server: bool = False) -> dict[str, Tool]:
51
  """
 
57
  """
58
  all_tools: dict[str, Tool] = {}
59
 
60
+ for mounted in self._mounted_servers:
61
  try:
62
  if via_server:
63
  # Use the server-to-server filtered path
 
201
  raise ToolError(f"Error calling tool {key!r}: {e}") from e
202
 
203
  # 2. Check mounted servers using the filtered protocol path.
204
+ for mounted in reversed(self._mounted_servers):
205
+ tool_key = key
206
+ if mounted.prefix:
207
+ if key.startswith(f"{mounted.prefix}_"):
208
+ tool_key = key.removeprefix(f"{mounted.prefix}_")
209
+ else:
210
  continue
211
+ try:
212
+ return await mounted.server._call_tool(tool_key, arguments)
213
+ except NotFoundError:
214
+ continue
215
 
216
  raise NotFoundError(f"Tool {key!r} not found.")
tests/prompts/test_prompt_manager.py CHANGED
@@ -10,7 +10,7 @@ from fastmcp.prompts.prompt_manager import PromptManager
10
 
11
 
12
  class TestPromptManager:
13
- def test_add_prompt(self):
14
  """Test adding a prompt to the manager."""
15
 
16
  def fn() -> str:
@@ -20,9 +20,9 @@ class TestPromptManager:
20
  prompt = Prompt.from_function(fn)
21
  added = manager.add_prompt(prompt)
22
  assert added == prompt
23
- assert manager.get_prompt("fn") == prompt
24
 
25
- def test_add_duplicate_prompt(self, caplog):
26
  """Test adding the same prompt twice."""
27
 
28
  def fn() -> str:
@@ -35,7 +35,7 @@ class TestPromptManager:
35
  assert first == second
36
  assert "Prompt already exists" in caplog.text
37
 
38
- def test_disable_warn_on_duplicate_prompts(self, caplog):
39
  """Test disabling warning on duplicate prompts."""
40
 
41
  def fn() -> str:
@@ -48,7 +48,7 @@ class TestPromptManager:
48
  assert first == second
49
  assert "Prompt already exists" not in caplog.text
50
 
51
- def test_warn_on_duplicate_prompts(self, caplog):
52
  """Test warning on duplicate prompts."""
53
  manager = PromptManager(duplicate_behavior="warn")
54
 
@@ -62,9 +62,9 @@ class TestPromptManager:
62
 
63
  assert "Prompt already exists: test_prompt" in caplog.text
64
  # Should have the prompt
65
- assert manager.get_prompt("test_prompt") is not None
66
 
67
- def test_error_on_duplicate_prompts(self):
68
  """Test error on duplicate prompts."""
69
  manager = PromptManager(duplicate_behavior="error")
70
 
@@ -78,7 +78,7 @@ class TestPromptManager:
78
  with pytest.raises(ValueError, match="Prompt already exists: test_prompt"):
79
  manager.add_prompt(prompt)
80
 
81
- def test_replace_duplicate_prompts(self):
82
  """Test replacing duplicate prompts."""
83
  manager = PromptManager(duplicate_behavior="replace")
84
 
@@ -95,12 +95,12 @@ class TestPromptManager:
95
  manager.add_prompt(prompt2)
96
 
97
  # Should have replaced with the new prompt
98
- prompt = manager.get_prompt("test_prompt")
99
  assert prompt is not None
100
  assert isinstance(prompt, FunctionPrompt)
101
  assert prompt.fn.__name__ == "replacement_fn"
102
 
103
- def test_ignore_duplicate_prompts(self):
104
  """Test ignoring duplicate prompts."""
105
  manager = PromptManager(duplicate_behavior="ignore")
106
 
@@ -117,7 +117,7 @@ class TestPromptManager:
117
  result = manager.add_prompt(prompt2)
118
 
119
  # Should keep the original
120
- prompt = manager.get_prompt("test_prompt")
121
  assert prompt is not None
122
  assert isinstance(prompt, FunctionPrompt)
123
  assert prompt.fn.__name__ == "original_fn"
@@ -125,7 +125,7 @@ class TestPromptManager:
125
  assert isinstance(result, FunctionPrompt)
126
  assert result.fn.__name__ == "original_fn"
127
 
128
- def test_get_prompts(self):
129
  """Test retrieving all prompts."""
130
 
131
  def fn1() -> str:
@@ -139,7 +139,7 @@ class TestPromptManager:
139
  prompt2 = Prompt.from_function(fn2)
140
  manager.add_prompt(prompt1)
141
  manager.add_prompt(prompt2)
142
- prompts = manager.get_prompts()
143
  assert len(prompts) == 2
144
  assert prompts["fn1"] == prompt1
145
  assert prompts["fn2"] == prompt2
@@ -270,7 +270,7 @@ class TestRenderPrompt:
270
  class TestPromptTags:
271
  """Test functionality related to prompt tags."""
272
 
273
- def test_add_prompt_with_tags(self):
274
  """Test adding a prompt with tags."""
275
 
276
  def greeting() -> str:
@@ -280,11 +280,11 @@ class TestPromptTags:
280
  prompt = Prompt.from_function(greeting, tags={"greeting", "simple"})
281
  manager.add_prompt(prompt)
282
 
283
- prompt = manager.get_prompt("greeting")
284
  assert prompt is not None
285
  assert prompt.tags == {"greeting", "simple"}
286
 
287
- def test_add_prompt_with_empty_tags(self):
288
  """Test adding a prompt with empty tags."""
289
 
290
  def greeting() -> str:
@@ -294,11 +294,11 @@ class TestPromptTags:
294
  prompt = Prompt.from_function(greeting, tags=set())
295
  manager.add_prompt(prompt)
296
 
297
- prompt = manager.get_prompt("greeting")
298
  assert prompt is not None
299
  assert prompt.tags == set()
300
 
301
- def test_add_prompt_with_none_tags(self):
302
  """Test adding a prompt with None tags."""
303
 
304
  def greeting() -> str:
@@ -308,11 +308,11 @@ class TestPromptTags:
308
  prompt = Prompt.from_function(greeting, tags=None)
309
  manager.add_prompt(prompt)
310
 
311
- prompt = manager.get_prompt("greeting")
312
  assert prompt is not None
313
  assert prompt.tags == set()
314
 
315
- def test_list_prompts_with_tags(self):
316
  """Test listing prompts with specific tags."""
317
 
318
  def greeting() -> str:
@@ -332,13 +332,12 @@ class TestPromptTags:
332
  )
333
 
334
  # Filter prompts by tags
335
- simple_prompts = [
336
- p for p in manager.get_prompts().values() if "simple" in p.tags
337
- ]
338
  assert len(simple_prompts) == 2
339
  assert {p.name for p in simple_prompts} == {"greeting", "summary"}
340
 
341
- nlp_prompts = [p for p in manager.get_prompts().values() if "nlp" in p.tags]
342
  assert len(nlp_prompts) == 1
343
  assert nlp_prompts[0].name == "summary"
344
 
 
10
 
11
 
12
  class TestPromptManager:
13
+ async def test_add_prompt(self):
14
  """Test adding a prompt to the manager."""
15
 
16
  def fn() -> str:
 
20
  prompt = Prompt.from_function(fn)
21
  added = manager.add_prompt(prompt)
22
  assert added == prompt
23
+ assert await manager.get_prompt("fn") == prompt
24
 
25
+ async def test_add_duplicate_prompt(self, caplog):
26
  """Test adding the same prompt twice."""
27
 
28
  def fn() -> str:
 
35
  assert first == second
36
  assert "Prompt already exists" in caplog.text
37
 
38
+ async def test_disable_warn_on_duplicate_prompts(self, caplog):
39
  """Test disabling warning on duplicate prompts."""
40
 
41
  def fn() -> str:
 
48
  assert first == second
49
  assert "Prompt already exists" not in caplog.text
50
 
51
+ async def test_warn_on_duplicate_prompts(self, caplog):
52
  """Test warning on duplicate prompts."""
53
  manager = PromptManager(duplicate_behavior="warn")
54
 
 
62
 
63
  assert "Prompt already exists: test_prompt" in caplog.text
64
  # Should have the prompt
65
+ assert await manager.get_prompt("test_prompt") is not None
66
 
67
+ async def test_error_on_duplicate_prompts(self):
68
  """Test error on duplicate prompts."""
69
  manager = PromptManager(duplicate_behavior="error")
70
 
 
78
  with pytest.raises(ValueError, match="Prompt already exists: test_prompt"):
79
  manager.add_prompt(prompt)
80
 
81
+ async def test_replace_duplicate_prompts(self):
82
  """Test replacing duplicate prompts."""
83
  manager = PromptManager(duplicate_behavior="replace")
84
 
 
95
  manager.add_prompt(prompt2)
96
 
97
  # Should have replaced with the new prompt
98
+ prompt = await manager.get_prompt("test_prompt")
99
  assert prompt is not None
100
  assert isinstance(prompt, FunctionPrompt)
101
  assert prompt.fn.__name__ == "replacement_fn"
102
 
103
+ async def test_ignore_duplicate_prompts(self):
104
  """Test ignoring duplicate prompts."""
105
  manager = PromptManager(duplicate_behavior="ignore")
106
 
 
117
  result = manager.add_prompt(prompt2)
118
 
119
  # Should keep the original
120
+ prompt = await manager.get_prompt("test_prompt")
121
  assert prompt is not None
122
  assert isinstance(prompt, FunctionPrompt)
123
  assert prompt.fn.__name__ == "original_fn"
 
125
  assert isinstance(result, FunctionPrompt)
126
  assert result.fn.__name__ == "original_fn"
127
 
128
+ async def test_get_prompts(self):
129
  """Test retrieving all prompts."""
130
 
131
  def fn1() -> str:
 
139
  prompt2 = Prompt.from_function(fn2)
140
  manager.add_prompt(prompt1)
141
  manager.add_prompt(prompt2)
142
+ prompts = await manager.get_prompts()
143
  assert len(prompts) == 2
144
  assert prompts["fn1"] == prompt1
145
  assert prompts["fn2"] == prompt2
 
270
  class TestPromptTags:
271
  """Test functionality related to prompt tags."""
272
 
273
+ async def test_add_prompt_with_tags(self):
274
  """Test adding a prompt with tags."""
275
 
276
  def greeting() -> str:
 
280
  prompt = Prompt.from_function(greeting, tags={"greeting", "simple"})
281
  manager.add_prompt(prompt)
282
 
283
+ prompt = await manager.get_prompt("greeting")
284
  assert prompt is not None
285
  assert prompt.tags == {"greeting", "simple"}
286
 
287
+ async def test_add_prompt_with_empty_tags(self):
288
  """Test adding a prompt with empty tags."""
289
 
290
  def greeting() -> str:
 
294
  prompt = Prompt.from_function(greeting, tags=set())
295
  manager.add_prompt(prompt)
296
 
297
+ prompt = await manager.get_prompt("greeting")
298
  assert prompt is not None
299
  assert prompt.tags == set()
300
 
301
+ async def test_add_prompt_with_none_tags(self):
302
  """Test adding a prompt with None tags."""
303
 
304
  def greeting() -> str:
 
308
  prompt = Prompt.from_function(greeting, tags=None)
309
  manager.add_prompt(prompt)
310
 
311
+ prompt = await manager.get_prompt("greeting")
312
  assert prompt is not None
313
  assert prompt.tags == set()
314
 
315
+ async def test_list_prompts_with_tags(self):
316
  """Test listing prompts with specific tags."""
317
 
318
  def greeting() -> str:
 
332
  )
333
 
334
  # Filter prompts by tags
335
+ prompts = await manager.get_prompts()
336
+ simple_prompts = [p for p in prompts.values() if "simple" in p.tags]
 
337
  assert len(simple_prompts) == 2
338
  assert {p.name for p in simple_prompts} == {"greeting", "summary"}
339
 
340
+ nlp_prompts = [p for p in prompts.values() if "nlp" in p.tags]
341
  assert len(nlp_prompts) == 1
342
  assert nlp_prompts[0].name == "summary"
343
 
tests/resources/test_resource_manager.py CHANGED
@@ -33,7 +33,7 @@ def temp_file():
33
  class TestResourceManager:
34
  """Test ResourceManager functionality."""
35
 
36
- def test_add_resource(self, temp_file: Path):
37
  """Test adding a resource."""
38
  manager = ResourceManager()
39
  file_url = "file://test-resource"
@@ -45,10 +45,11 @@ class TestResourceManager:
45
  added = manager.add_resource(resource)
46
  assert added == resource
47
  # Get the actual key from the resource manager
48
- assert len(manager.get_resources()) == 1
49
- assert resource in manager.get_resources().values()
 
50
 
51
- def test_add_duplicate_resource(self, temp_file: Path):
52
  """Test adding the same resource twice."""
53
  manager = ResourceManager()
54
  file_url = "file://test-resource"
@@ -61,10 +62,11 @@ class TestResourceManager:
61
  second = manager.add_resource(resource)
62
  assert first == second
63
  # Check the resource is there
64
- assert len(manager.get_resources()) == 1
65
- assert resource in manager.get_resources().values()
 
66
 
67
- def test_warn_on_duplicate_resources(self, temp_file: Path, caplog):
68
  """Test warning on duplicate resources."""
69
  manager = ResourceManager(duplicate_behavior="warn")
70
 
@@ -80,10 +82,11 @@ class TestResourceManager:
80
 
81
  assert "Resource already exists" in caplog.text
82
  # Should have the resource
83
- assert len(manager.get_resources()) == 1
84
- assert resource in manager.get_resources().values()
 
85
 
86
- def test_disable_warn_on_duplicate_resources(self, temp_file: Path, caplog):
87
  """Test disabling warning on duplicate resources."""
88
  manager = ResourceManager(duplicate_behavior="ignore")
89
  resource = FileResource(
@@ -95,7 +98,7 @@ class TestResourceManager:
95
  manager.add_resource(resource)
96
  assert "Resource already exists" not in caplog.text
97
 
98
- def test_error_on_duplicate_resources(self, temp_file: Path):
99
  """Test error on duplicate resources."""
100
  manager = ResourceManager(duplicate_behavior="error")
101
 
@@ -110,7 +113,7 @@ class TestResourceManager:
110
  with pytest.raises(ValueError, match="Resource already exists"):
111
  manager.add_resource(resource)
112
 
113
- def test_replace_duplicate_resources(self, temp_file: Path):
114
  """Test replacing duplicate resources."""
115
  manager = ResourceManager(duplicate_behavior="replace")
116
 
@@ -131,11 +134,12 @@ class TestResourceManager:
131
  manager.add_resource(resource2)
132
 
133
  # Should have replaced with the new resource
134
- resources = list(manager.get_resources().values())
135
- assert len(resources) == 1
136
- assert resources[0].name == "replacement"
 
137
 
138
- def test_ignore_duplicate_resources(self, temp_file: Path):
139
  """Test ignoring duplicate resources."""
140
  manager = ResourceManager(duplicate_behavior="ignore")
141
 
@@ -156,13 +160,14 @@ class TestResourceManager:
156
  result = manager.add_resource(resource2)
157
 
158
  # Should keep the original
159
- resources = list(manager.get_resources().values())
160
- assert len(resources) == 1
161
- assert resources[0].name == "original"
 
162
  # Result should be the original resource
163
  assert result.name == "original"
164
 
165
- def test_warn_on_duplicate_templates(self, caplog):
166
  """Test warning on duplicate templates."""
167
  manager = ResourceManager(duplicate_behavior="warn")
168
 
@@ -180,9 +185,10 @@ class TestResourceManager:
180
 
181
  assert "Template already exists" in caplog.text
182
  # Should have the template
183
- assert manager.get_resource_templates() == {"test://{id}": template}
 
184
 
185
- def test_error_on_duplicate_templates(self):
186
  """Test error on duplicate templates."""
187
  manager = ResourceManager(duplicate_behavior="error")
188
 
@@ -200,7 +206,7 @@ class TestResourceManager:
200
  with pytest.raises(ValueError, match="Template already exists"):
201
  manager.add_template(template)
202
 
203
- def test_replace_duplicate_templates(self):
204
  """Test replacing duplicate templates."""
205
  manager = ResourceManager(duplicate_behavior="replace")
206
 
@@ -226,11 +232,12 @@ class TestResourceManager:
226
  manager.add_template(template2)
227
 
228
  # Should have replaced with the new template
229
- templates = list(manager.get_resource_templates().values())
 
230
  assert len(templates) == 1
231
  assert templates[0].name == "replacement"
232
 
233
- def test_ignore_duplicate_templates(self):
234
  """Test ignoring duplicate templates."""
235
  manager = ResourceManager(duplicate_behavior="ignore")
236
 
@@ -256,7 +263,8 @@ class TestResourceManager:
256
  result = manager.add_template(template2)
257
 
258
  # Should keep the original
259
- templates = list(manager.get_resource_templates().values())
 
260
  assert len(templates) == 1
261
  assert templates[0].name == "original"
262
  # Result should be the original template
@@ -299,7 +307,7 @@ class TestResourceManager:
299
  with pytest.raises(NotFoundError, match="Unknown resource"):
300
  await manager.get_resource(AnyUrl("unknown://test"))
301
 
302
- def test_get_resources(self, temp_file: Path):
303
  """Test retrieving all resources."""
304
  manager = ResourceManager()
305
  file_url1 = "file://test-resource1"
@@ -316,7 +324,7 @@ class TestResourceManager:
316
  )
317
  manager.add_resource(resource1)
318
  manager.add_resource(resource2)
319
- resources = manager.get_resources()
320
  assert len(resources) == 2
321
  values = list(resources.values())
322
  assert resource1 in values
@@ -326,7 +334,7 @@ class TestResourceManager:
326
  class TestResourceTags:
327
  """Test functionality related to resource tags."""
328
 
329
- def test_add_resource_with_tags(self, temp_file: Path):
330
  """Test adding a resource with tags."""
331
  manager = ResourceManager()
332
  resource = FileResource(
@@ -338,11 +346,12 @@ class TestResourceTags:
338
  manager.add_resource(resource)
339
 
340
  # Check that tags are preserved
341
- resources = list(manager.get_resources().values())
 
342
  assert len(resources) == 1
343
  assert resources[0].tags == {"weather", "data"}
344
 
345
- def test_add_function_resource_with_tags(self):
346
  """Test adding a function resource with tags."""
347
  manager = ResourceManager()
348
 
@@ -359,11 +368,12 @@ class TestResourceTags:
359
  )
360
 
361
  manager.add_resource(resource)
362
- resources = list(manager.get_resources().values())
 
363
  assert len(resources) == 1
364
  assert resources[0].tags == {"sample", "test", "data"}
365
 
366
- def test_add_template_with_tags(self):
367
  """Test adding a resource template with tags."""
368
  manager = ResourceManager()
369
 
@@ -379,11 +389,12 @@ class TestResourceTags:
379
  )
380
 
381
  manager.add_template(template)
382
- templates = list(manager.get_resource_templates().values())
 
383
  assert len(templates) == 1
384
  assert templates[0].tags == {"users", "template", "data"}
385
 
386
- def test_filter_resources_by_tags(self, temp_file: Path):
387
  """Test filtering resources by tags."""
388
  manager = ResourceManager()
389
 
@@ -392,7 +403,7 @@ class TestResourceTags:
392
  uri=FileUrl("file://weather-data"),
393
  name="weather_data",
394
  path=temp_file,
395
- tags={"weather", "external"},
396
  )
397
 
398
  async def get_user_data():
@@ -401,8 +412,10 @@ class TestResourceTags:
401
  resource2 = FunctionResource(
402
  uri=AnyUrl("data://users"),
403
  name="user_data",
 
 
404
  fn=get_user_data,
405
- tags={"users", "internal"},
406
  )
407
 
408
  async def get_system_data():
@@ -411,26 +424,25 @@ class TestResourceTags:
411
  resource3 = FunctionResource(
412
  uri=AnyUrl("data://system"),
413
  name="system_data",
 
 
414
  fn=get_system_data,
415
- tags={"system", "internal"},
416
  )
417
 
418
  manager.add_resource(resource1)
419
  manager.add_resource(resource2)
420
  manager.add_resource(resource3)
421
 
422
- # Filter resources by tags
423
- internal_resources = [
424
- r for r in manager.get_resources().values() if "internal" in r.tags
425
- ]
426
- assert len(internal_resources) == 2
427
- assert {r.name for r in internal_resources} == {"user_data", "system_data"}
428
-
429
- external_resources = [
430
- r for r in manager.get_resources().values() if "external" in r.tags
431
- ]
432
- assert len(external_resources) == 1
433
- assert external_resources[0].name == "weather_data"
434
 
435
 
436
  class TestCustomResourceKeys:
 
33
  class TestResourceManager:
34
  """Test ResourceManager functionality."""
35
 
36
+ async def test_add_resource(self, temp_file: Path):
37
  """Test adding a resource."""
38
  manager = ResourceManager()
39
  file_url = "file://test-resource"
 
45
  added = manager.add_resource(resource)
46
  assert added == resource
47
  # Get the actual key from the resource manager
48
+ resources = await manager.get_resources()
49
+ assert len(resources) == 1
50
+ assert resource in resources.values()
51
 
52
+ async def test_add_duplicate_resource(self, temp_file: Path):
53
  """Test adding the same resource twice."""
54
  manager = ResourceManager()
55
  file_url = "file://test-resource"
 
62
  second = manager.add_resource(resource)
63
  assert first == second
64
  # Check the resource is there
65
+ resources = await manager.get_resources()
66
+ assert len(resources) == 1
67
+ assert resource in resources.values()
68
 
69
+ async def test_warn_on_duplicate_resources(self, temp_file: Path, caplog):
70
  """Test warning on duplicate resources."""
71
  manager = ResourceManager(duplicate_behavior="warn")
72
 
 
82
 
83
  assert "Resource already exists" in caplog.text
84
  # Should have the resource
85
+ resources = await manager.get_resources()
86
+ assert len(resources) == 1
87
+ assert resource in resources.values()
88
 
89
+ async def test_disable_warn_on_duplicate_resources(self, temp_file: Path, caplog):
90
  """Test disabling warning on duplicate resources."""
91
  manager = ResourceManager(duplicate_behavior="ignore")
92
  resource = FileResource(
 
98
  manager.add_resource(resource)
99
  assert "Resource already exists" not in caplog.text
100
 
101
+ async def test_error_on_duplicate_resources(self, temp_file: Path):
102
  """Test error on duplicate resources."""
103
  manager = ResourceManager(duplicate_behavior="error")
104
 
 
113
  with pytest.raises(ValueError, match="Resource already exists"):
114
  manager.add_resource(resource)
115
 
116
+ async def test_replace_duplicate_resources(self, temp_file: Path):
117
  """Test replacing duplicate resources."""
118
  manager = ResourceManager(duplicate_behavior="replace")
119
 
 
134
  manager.add_resource(resource2)
135
 
136
  # Should have replaced with the new resource
137
+ resources = await manager.get_resources()
138
+ resource_list = list(resources.values())
139
+ assert len(resource_list) == 1
140
+ assert resource_list[0].name == "replacement"
141
 
142
+ async def test_ignore_duplicate_resources(self, temp_file: Path):
143
  """Test ignoring duplicate resources."""
144
  manager = ResourceManager(duplicate_behavior="ignore")
145
 
 
160
  result = manager.add_resource(resource2)
161
 
162
  # Should keep the original
163
+ resources = await manager.get_resources()
164
+ resource_list = list(resources.values())
165
+ assert len(resource_list) == 1
166
+ assert resource_list[0].name == "original"
167
  # Result should be the original resource
168
  assert result.name == "original"
169
 
170
+ async def test_warn_on_duplicate_templates(self, caplog):
171
  """Test warning on duplicate templates."""
172
  manager = ResourceManager(duplicate_behavior="warn")
173
 
 
185
 
186
  assert "Template already exists" in caplog.text
187
  # Should have the template
188
+ templates = await manager.get_resource_templates()
189
+ assert templates == {"test://{id}": template}
190
 
191
+ async def test_error_on_duplicate_templates(self):
192
  """Test error on duplicate templates."""
193
  manager = ResourceManager(duplicate_behavior="error")
194
 
 
206
  with pytest.raises(ValueError, match="Template already exists"):
207
  manager.add_template(template)
208
 
209
+ async def test_replace_duplicate_templates(self):
210
  """Test replacing duplicate templates."""
211
  manager = ResourceManager(duplicate_behavior="replace")
212
 
 
232
  manager.add_template(template2)
233
 
234
  # Should have replaced with the new template
235
+ templates_dict = await manager.get_resource_templates()
236
+ templates = list(templates_dict.values())
237
  assert len(templates) == 1
238
  assert templates[0].name == "replacement"
239
 
240
+ async def test_ignore_duplicate_templates(self):
241
  """Test ignoring duplicate templates."""
242
  manager = ResourceManager(duplicate_behavior="ignore")
243
 
 
263
  result = manager.add_template(template2)
264
 
265
  # Should keep the original
266
+ templates_dict = await manager.get_resource_templates()
267
+ templates = list(templates_dict.values())
268
  assert len(templates) == 1
269
  assert templates[0].name == "original"
270
  # Result should be the original template
 
307
  with pytest.raises(NotFoundError, match="Unknown resource"):
308
  await manager.get_resource(AnyUrl("unknown://test"))
309
 
310
+ async def test_get_resources(self, temp_file: Path):
311
  """Test retrieving all resources."""
312
  manager = ResourceManager()
313
  file_url1 = "file://test-resource1"
 
324
  )
325
  manager.add_resource(resource1)
326
  manager.add_resource(resource2)
327
+ resources = await manager.get_resources()
328
  assert len(resources) == 2
329
  values = list(resources.values())
330
  assert resource1 in values
 
334
  class TestResourceTags:
335
  """Test functionality related to resource tags."""
336
 
337
+ async def test_add_resource_with_tags(self, temp_file: Path):
338
  """Test adding a resource with tags."""
339
  manager = ResourceManager()
340
  resource = FileResource(
 
346
  manager.add_resource(resource)
347
 
348
  # Check that tags are preserved
349
+ resources_dict = await manager.get_resources()
350
+ resources = list(resources_dict.values())
351
  assert len(resources) == 1
352
  assert resources[0].tags == {"weather", "data"}
353
 
354
+ async def test_add_function_resource_with_tags(self):
355
  """Test adding a function resource with tags."""
356
  manager = ResourceManager()
357
 
 
368
  )
369
 
370
  manager.add_resource(resource)
371
+ resources_dict = await manager.get_resources()
372
+ resources = list(resources_dict.values())
373
  assert len(resources) == 1
374
  assert resources[0].tags == {"sample", "test", "data"}
375
 
376
+ async def test_add_template_with_tags(self):
377
  """Test adding a resource template with tags."""
378
  manager = ResourceManager()
379
 
 
389
  )
390
 
391
  manager.add_template(template)
392
+ templates_dict = await manager.get_resource_templates()
393
+ templates = list(templates_dict.values())
394
  assert len(templates) == 1
395
  assert templates[0].tags == {"users", "template", "data"}
396
 
397
+ async def test_filter_resources_by_tags(self, temp_file: Path):
398
  """Test filtering resources by tags."""
399
  manager = ResourceManager()
400
 
 
403
  uri=FileUrl("file://weather-data"),
404
  name="weather_data",
405
  path=temp_file,
406
+ tags={"weather", "data"},
407
  )
408
 
409
  async def get_user_data():
 
412
  resource2 = FunctionResource(
413
  uri=AnyUrl("data://users"),
414
  name="user_data",
415
+ description="User data resource",
416
+ mime_type="text/plain",
417
  fn=get_user_data,
418
+ tags={"users", "data"},
419
  )
420
 
421
  async def get_system_data():
 
424
  resource3 = FunctionResource(
425
  uri=AnyUrl("data://system"),
426
  name="system_data",
427
+ description="System data resource",
428
+ mime_type="text/plain",
429
  fn=get_system_data,
430
+ tags={"system", "admin"},
431
  )
432
 
433
  manager.add_resource(resource1)
434
  manager.add_resource(resource2)
435
  manager.add_resource(resource3)
436
 
437
+ # Filter by tags
438
+ resources_dict = await manager.get_resources()
439
+ data_resources = [r for r in resources_dict.values() if "data" in r.tags]
440
+ assert len(data_resources) == 2
441
+ assert {r.name for r in data_resources} == {"weather_data", "user_data"}
442
+
443
+ admin_resources = [r for r in resources_dict.values() if "admin" in r.tags]
444
+ assert len(admin_resources) == 1
445
+ assert admin_resources[0].name == "system_data"
 
 
 
446
 
447
 
448
  class TestCustomResourceKeys:
tests/resources/test_resource_template.py CHANGED
@@ -405,6 +405,7 @@ class TestMatchUriTemplate:
405
  ("test://a/b/c", None),
406
  ("test://a/x/b", {"x": "x"}),
407
  ("test://a/x/y/b", None),
 
408
  ],
409
  )
410
  def test_match_uri_template_single_param(
 
405
  ("test://a/b/c", None),
406
  ("test://a/x/b", {"x": "x"}),
407
  ("test://a/x/y/b", None),
408
+ ("test://a/1-2/b", {"x": "1-2"}),
409
  ],
410
  )
411
  def test_match_uri_template_single_param(
tests/server/openapi/test_openapi.py CHANGED
@@ -136,7 +136,7 @@ def api_client(fastapi_app: FastAPI) -> AsyncClient:
136
 
137
 
138
  @pytest.fixture
139
- async def fastmcp_openapi_server_with_all_types(
140
  fastapi_app: FastAPI, api_client: httpx.AsyncClient
141
  ) -> FastMCPOpenAPI:
142
  openapi_spec = fastapi_app.openapi()
@@ -213,13 +213,11 @@ class TestTools:
213
  assert len(await server.get_resources()) == 0
214
  assert len(await server.get_resource_templates()) == 0
215
 
216
- async def test_list_tools(
217
- self, fastmcp_openapi_server_with_all_types: FastMCPOpenAPI
218
- ):
219
  """
220
  By default, tools exclude GET methods
221
  """
222
- async with Client(fastmcp_openapi_server_with_all_types) as client:
223
  tools = await client.list_tools()
224
  assert len(tools) == 2
225
 
@@ -254,13 +252,13 @@ class TestTools:
254
 
255
  async def test_call_create_user_tool(
256
  self,
257
- fastmcp_openapi_server_with_all_types: FastMCPOpenAPI,
258
  api_client,
259
  ):
260
  """
261
  The tool created by the OpenAPI server should be the same as the original
262
  """
263
- async with Client(fastmcp_openapi_server_with_all_types) as client:
264
  tool_response = await client.call_tool(
265
  "create_user_users_post", {"name": "David", "active": False}
266
  )
@@ -274,7 +272,7 @@ class TestTools:
274
  assert len(response.json()) == 4
275
 
276
  # Check that the user was created via MCP
277
- async with Client(fastmcp_openapi_server_with_all_types) as client:
278
  user_response = await client.read_resource("resource://get_user_users/4")
279
  response_text = user_response[0].text # type: ignore[attr-defined]
280
  user = json.loads(response_text)
@@ -282,13 +280,13 @@ class TestTools:
282
 
283
  async def test_call_update_user_name_tool(
284
  self,
285
- fastmcp_openapi_server_with_all_types: FastMCPOpenAPI,
286
  api_client,
287
  ):
288
  """
289
  The tool created by the OpenAPI server should be the same as the original
290
  """
291
- async with Client(fastmcp_openapi_server_with_all_types) as client:
292
  tool_response = await client.call_tool(
293
  "update_user_name_users",
294
  {"user_id": 1, "name": "XYZ"},
@@ -303,7 +301,7 @@ class TestTools:
303
  assert expected_data in response.json()
304
 
305
  # Check that the user was updated via MCP
306
- async with Client(fastmcp_openapi_server_with_all_types) as client:
307
  user_response = await client.read_resource("resource://get_user_users/1")
308
  response_text = user_response[0].text # type: ignore[attr-defined]
309
  user = json.loads(response_text)
@@ -335,13 +333,11 @@ class TestTools:
335
 
336
 
337
  class TestResources:
338
- async def test_list_resources(
339
- self, fastmcp_openapi_server_with_all_types: FastMCPOpenAPI
340
- ):
341
  """
342
  By default, resources exclude GET methods without parameters
343
  """
344
- async with Client(fastmcp_openapi_server_with_all_types) as client:
345
  resources = await client.list_resources()
346
  assert len(resources) == 4
347
  assert resources[0].uri == AnyUrl("resource://get_users_users_get")
@@ -349,7 +345,7 @@ class TestResources:
349
 
350
  async def test_get_resource(
351
  self,
352
- fastmcp_openapi_server_with_all_types: FastMCPOpenAPI,
353
  api_client,
354
  users_db: dict[int, User],
355
  ):
@@ -360,7 +356,7 @@ class TestResources:
360
  json_users = TypeAdapter(list[User]).dump_python(
361
  sorted(users_db.values(), key=lambda x: x.id)
362
  )
363
- async with Client(fastmcp_openapi_server_with_all_types) as client:
364
  resource_response = await client.read_resource(
365
  "resource://get_users_users_get"
366
  )
@@ -372,11 +368,11 @@ class TestResources:
372
 
373
  async def test_get_bytes_resource(
374
  self,
375
- fastmcp_openapi_server_with_all_types: FastMCPOpenAPI,
376
  api_client,
377
  ):
378
  """Test reading a resource that returns bytes."""
379
- async with Client(fastmcp_openapi_server_with_all_types) as client:
380
  resource_response = await client.read_resource(
381
  "resource://ping_bytes_ping_bytes_get"
382
  )
@@ -385,23 +381,23 @@ class TestResources:
385
 
386
  async def test_get_str_resource(
387
  self,
388
- fastmcp_openapi_server_with_all_types: FastMCPOpenAPI,
389
  api_client,
390
  ):
391
  """Test reading a resource that returns a string."""
392
- async with Client(fastmcp_openapi_server_with_all_types) as client:
393
  resource_response = await client.read_resource("resource://ping_ping_get")
394
  assert resource_response[0].text == "pong" # type: ignore[attr-defined]
395
 
396
 
397
  class TestResourceTemplates:
398
  async def test_list_resource_templates(
399
- self, fastmcp_openapi_server_with_all_types: FastMCPOpenAPI
400
  ):
401
  """
402
  By default, resource templates exclude GET methods without parameters
403
  """
404
- async with Client(fastmcp_openapi_server_with_all_types) as client:
405
  resource_templates = await client.list_resource_templates()
406
  assert len(resource_templates) == 2
407
  assert resource_templates[0].name == "get_user_users"
@@ -416,7 +412,7 @@ class TestResourceTemplates:
416
 
417
  async def test_get_resource_template(
418
  self,
419
- fastmcp_openapi_server_with_all_types: FastMCPOpenAPI,
420
  api_client,
421
  users_db: dict[int, User],
422
  ):
@@ -424,7 +420,7 @@ class TestResourceTemplates:
424
  The resource template created by the OpenAPI server should be the same as the original
425
  """
426
  user_id = 2
427
- async with Client(fastmcp_openapi_server_with_all_types) as client:
428
  resource_response = await client.read_resource(
429
  f"resource://get_user_users/{user_id}"
430
  )
@@ -437,7 +433,7 @@ class TestResourceTemplates:
437
 
438
  async def test_get_resource_template_multi_param(
439
  self,
440
- fastmcp_openapi_server_with_all_types: FastMCPOpenAPI,
441
  api_client,
442
  users_db: dict[int, User],
443
  ):
@@ -446,7 +442,7 @@ class TestResourceTemplates:
446
  """
447
  user_id = 2
448
  is_active = True
449
- async with Client(fastmcp_openapi_server_with_all_types) as client:
450
  resource_response = await client.read_resource(
451
  f"resource://get_user_active_state_users/{is_active}/{user_id}"
452
  )
@@ -459,13 +455,11 @@ class TestResourceTemplates:
459
 
460
 
461
  class TestPrompts:
462
- async def test_list_prompts(
463
- self, fastmcp_openapi_server_with_all_types: FastMCPOpenAPI
464
- ):
465
  """
466
  By default, there are no prompts.
467
  """
468
- async with Client(fastmcp_openapi_server_with_all_types) as client:
469
  prompts = await client.list_prompts()
470
  assert len(prompts) == 0
471
 
@@ -474,11 +468,11 @@ class TestTagTransfer:
474
  """Tests for transferring tags from OpenAPI routes to MCP objects."""
475
 
476
  async def test_tags_transferred_to_tools(
477
- self, fastmcp_openapi_server_with_all_types: FastMCPOpenAPI
478
  ):
479
  """Test that tags from OpenAPI routes are correctly transferred to Tools."""
480
  # Get internal tools directly (not the public API which returns MCP.Content)
481
- tools = fastmcp_openapi_server_with_all_types._tool_manager._list_tools()
482
 
483
  # Find the create_user and update_user_name tools
484
  create_user_tool = next(
@@ -502,13 +496,12 @@ class TestTagTransfer:
502
  assert len(update_user_tool.tags) == 2
503
 
504
  async def test_tags_transferred_to_resources(
505
- self, fastmcp_openapi_server_with_all_types: FastMCPOpenAPI
506
  ):
507
  """Test that tags from OpenAPI routes are correctly transferred to Resources."""
508
  # Get internal resources directly
509
- resources = list(
510
- fastmcp_openapi_server_with_all_types._resource_manager.get_resources().values()
511
- )
512
 
513
  # Find the get_users resource
514
  get_users_resource = next(
@@ -523,13 +516,14 @@ class TestTagTransfer:
523
  assert len(get_users_resource.tags) == 2
524
 
525
  async def test_tags_transferred_to_resource_templates(
526
- self, fastmcp_openapi_server_with_all_types: FastMCPOpenAPI
527
  ):
528
  """Test that tags from OpenAPI routes are correctly transferred to ResourceTemplates."""
529
  # Get internal resource templates directly
530
- templates = list(
531
- fastmcp_openapi_server_with_all_types._resource_manager.get_resource_templates().values()
532
  )
 
533
 
534
  # Find the get_user template
535
  get_user_template = next(
@@ -544,13 +538,14 @@ class TestTagTransfer:
544
  assert len(get_user_template.tags) == 2
545
 
546
  async def test_tags_preserved_in_resources_created_from_templates(
547
- self, fastmcp_openapi_server_with_all_types: FastMCPOpenAPI
548
  ):
549
  """Test that tags are preserved when creating resources from templates."""
550
  # Get internal resource templates directly
551
- templates = list(
552
- fastmcp_openapi_server_with_all_types._resource_manager.get_resource_templates().values()
553
  )
 
554
 
555
  # Find the get_user template
556
  get_user_template = next(
@@ -1167,7 +1162,7 @@ class TestDescriptionPropagation:
1167
  return httpx.AsyncClient(transport=transport, base_url="http://test")
1168
 
1169
  @pytest.fixture
1170
- async def simple_server_with_all_types(self, simple_openapi_spec, mock_client):
1171
  """Create a FastMCPOpenAPI server with the simple test spec."""
1172
  return FastMCPOpenAPI(
1173
  openapi_spec=simple_openapi_spec,
@@ -1179,11 +1174,11 @@ class TestDescriptionPropagation:
1179
  # --- RESOURCE TESTS ---
1180
 
1181
  async def test_resource_includes_route_description(
1182
- self, simple_server_with_all_types
1183
  ):
1184
  """Test that a Resource includes the route description."""
1185
  resources = list(
1186
- simple_server_with_all_types._resource_manager.get_resources().values()
1187
  )
1188
  list_resource = next((r for r in resources if r.name == "listItems"), None)
1189
 
@@ -1193,11 +1188,11 @@ class TestDescriptionPropagation:
1193
  )
1194
 
1195
  async def test_resource_includes_response_description(
1196
- self, simple_server_with_all_types
1197
  ):
1198
  """Test that a Resource includes the response description."""
1199
  resources = list(
1200
- simple_server_with_all_types._resource_manager.get_resources().values()
1201
  )
1202
  list_resource = next((r for r in resources if r.name == "listItems"), None)
1203
 
@@ -1207,11 +1202,11 @@ class TestDescriptionPropagation:
1207
  )
1208
 
1209
  async def test_resource_includes_response_model_fields(
1210
- self, simple_server_with_all_types
1211
  ):
1212
  """Test that a Resource description includes response model field descriptions."""
1213
  resources = list(
1214
- simple_server_with_all_types._resource_manager.get_resources().values()
1215
  )
1216
  list_resource = next((r for r in resources if r.name == "listItems"), None)
1217
 
@@ -1230,12 +1225,13 @@ class TestDescriptionPropagation:
1230
  # --- RESOURCE TEMPLATE TESTS ---
1231
 
1232
  async def test_template_includes_route_description(
1233
- self, simple_server_with_all_types
1234
  ):
1235
  """Test that a ResourceTemplate includes the route description."""
1236
- templates = list(
1237
- simple_server_with_all_types._resource_manager.get_templates().values()
1238
  )
 
1239
  get_template = next((t for t in templates if t.name == "getItem"), None)
1240
 
1241
  assert get_template is not None, "getItem template wasn't created"
@@ -1244,12 +1240,13 @@ class TestDescriptionPropagation:
1244
  )
1245
 
1246
  async def test_template_includes_function_docstring(
1247
- self, simple_server_with_all_types
1248
  ):
1249
  """Test that a ResourceTemplate includes the function docstring."""
1250
- templates = list(
1251
- simple_server_with_all_types._resource_manager.get_templates().values()
1252
  )
 
1253
  get_template = next((t for t in templates if t.name == "getItem"), None)
1254
 
1255
  assert get_template is not None, "getItem template wasn't created"
@@ -1258,12 +1255,13 @@ class TestDescriptionPropagation:
1258
  )
1259
 
1260
  async def test_template_includes_path_parameter_description(
1261
- self, simple_server_with_all_types
1262
  ):
1263
  """Test that a ResourceTemplate includes path parameter descriptions."""
1264
- templates = list(
1265
- simple_server_with_all_types._resource_manager.get_templates().values()
1266
  )
 
1267
  get_template = next((t for t in templates if t.name == "getItem"), None)
1268
 
1269
  assert get_template is not None, "getItem template wasn't created"
@@ -1272,12 +1270,13 @@ class TestDescriptionPropagation:
1272
  )
1273
 
1274
  async def test_template_includes_query_parameter_description(
1275
- self, simple_server_with_all_types
1276
  ):
1277
  """Test that a ResourceTemplate includes query parameter descriptions."""
1278
- templates = list(
1279
- simple_server_with_all_types._resource_manager.get_templates().values()
1280
  )
 
1281
  get_template = next((t for t in templates if t.name == "getItem"), None)
1282
 
1283
  assert get_template is not None, "getItem template wasn't created"
@@ -1286,12 +1285,13 @@ class TestDescriptionPropagation:
1286
  )
1287
 
1288
  async def test_template_parameter_schema_includes_description(
1289
- self, simple_server_with_all_types
1290
  ):
1291
  """Test that a ResourceTemplate's parameter schema includes parameter descriptions."""
1292
- templates = list(
1293
- simple_server_with_all_types._resource_manager.get_templates().values()
1294
  )
 
1295
  get_template = next((t for t in templates if t.name == "getItem"), None)
1296
 
1297
  assert get_template is not None, "getItem template wasn't created"
@@ -1311,9 +1311,10 @@ class TestDescriptionPropagation:
1311
 
1312
  # --- TOOL TESTS ---
1313
 
1314
- async def test_tool_includes_route_description(self, simple_server_with_all_types):
1315
  """Test that a Tool includes the route description."""
1316
- tools = simple_server_with_all_types._tool_manager.list_tools()
 
1317
  create_tool = next((t for t in tools if t.name == "createItem"), None)
1318
 
1319
  assert create_tool is not None, "createItem tool wasn't created"
@@ -1321,9 +1322,10 @@ class TestDescriptionPropagation:
1321
  "Route description missing from Tool"
1322
  )
1323
 
1324
- async def test_tool_includes_function_docstring(self, simple_server_with_all_types):
1325
  """Test that a Tool includes the function docstring."""
1326
- tools = simple_server_with_all_types._tool_manager.list_tools()
 
1327
  create_tool = next((t for t in tools if t.name == "createItem"), None)
1328
 
1329
  assert create_tool is not None, "createItem tool wasn't created"
@@ -1333,10 +1335,11 @@ class TestDescriptionPropagation:
1333
  )
1334
 
1335
  async def test_tool_parameter_schema_includes_property_description(
1336
- self, simple_server_with_all_types
1337
  ):
1338
  """Test that a Tool's parameter schema includes property descriptions from request model."""
1339
- tools = simple_server_with_all_types._tool_manager.list_tools()
 
1340
  create_tool = next((t for t in tools if t.name == "createItem"), None)
1341
 
1342
  assert create_tool is not None, "createItem tool wasn't created"
@@ -1356,9 +1359,9 @@ class TestDescriptionPropagation:
1356
 
1357
  # --- CLIENT API TESTS ---
1358
 
1359
- async def test_client_api_resource_description(self, simple_server_with_all_types):
1360
  """Test that Resource descriptions are accessible via the client API."""
1361
- async with Client(simple_server_with_all_types) as client:
1362
  resources = await client.list_resources()
1363
  list_resource = next((r for r in resources if r.name == "listItems"), None)
1364
 
@@ -1370,9 +1373,9 @@ class TestDescriptionPropagation:
1370
  "Route description missing in Resource from client API"
1371
  )
1372
 
1373
- async def test_client_api_template_description(self, simple_server_with_all_types):
1374
  """Test that ResourceTemplate descriptions are accessible via the client API."""
1375
- async with Client(simple_server_with_all_types) as client:
1376
  templates = await client.list_resource_templates()
1377
  get_template = next((t for t in templates if t.name == "getItem"), None)
1378
 
@@ -1384,9 +1387,9 @@ class TestDescriptionPropagation:
1384
  "Route description missing in ResourceTemplate from client API"
1385
  )
1386
 
1387
- async def test_client_api_tool_description(self, simple_server_with_all_types):
1388
  """Test that Tool descriptions are accessible via the client API."""
1389
- async with Client(simple_server_with_all_types) as client:
1390
  tools = await client.list_tools()
1391
  create_tool = next((t for t in tools if t.name == "createItem"), None)
1392
 
@@ -1398,9 +1401,9 @@ class TestDescriptionPropagation:
1398
  "Function docstring missing in Tool from client API"
1399
  )
1400
 
1401
- async def test_client_api_tool_parameter_schema(self, simple_server_with_all_types):
1402
  """Test that Tool parameter schemas are accessible via the client API."""
1403
- async with Client(simple_server_with_all_types) as client:
1404
  tools = await client.list_tools()
1405
  create_tool = next((t for t in tools if t.name == "createItem"), None)
1406
 
@@ -1533,22 +1536,26 @@ class TestFastAPIDescriptionPropagation:
1533
 
1534
  # Debug: print all components created
1535
  print("\nDEBUG - Resources created:")
1536
- for name, resource in server._resource_manager.get_resources().items():
 
1537
  print(f" Resource: {name}, Name attribute: {resource.name}")
1538
 
1539
  print("\nDEBUG - Templates created:")
1540
- for name, template in server._resource_manager.get_resource_templates().items():
 
1541
  print(f" Template: {name}, Name attribute: {template.name}")
1542
 
1543
  print("\nDEBUG - Tools created:")
1544
- for tool in server._tool_manager._list_tools():
 
1545
  print(f" Tool: {tool.name}")
1546
 
1547
  return server
1548
 
1549
- async def test_resource_includes_function_docstring(self, fastapi_server):
1550
  """Test that a Resource includes the function docstring."""
1551
- resources = list(fastapi_server._resource_manager.get_resources().values())
 
1552
 
1553
  # Now checking for the get_items operation ID rather than list_items
1554
  list_resource = next((r for r in resources if "items_get" in r.name), None)
@@ -1559,13 +1566,16 @@ class TestFastAPIDescriptionPropagation:
1559
  "Function docstring missing from Resource"
1560
  )
1561
 
1562
- async def test_resource_includes_response_model_fields(self, fastapi_server):
 
 
1563
  """Test that a Resource description includes basic response information.
1564
 
1565
  Note: FastAPI doesn't reliably include Pydantic field descriptions in the OpenAPI schema,
1566
  so we can only check for basic response information being present.
1567
  """
1568
- resources = list(fastapi_server._resource_manager.get_resources().values())
 
1569
  list_resource = next((r for r in resources if "items_get" in r.name), None)
1570
 
1571
  assert list_resource is not None, "GET /items resource wasn't created"
@@ -1579,9 +1589,10 @@ class TestFastAPIDescriptionPropagation:
1579
  # We've already verified in TestDescriptionPropagation that when descriptions
1580
  # are present in the OpenAPI schema, they are properly included in the component description
1581
 
1582
- async def test_template_includes_function_docstring(self, fastapi_server):
1583
  """Test that a ResourceTemplate includes the function docstring."""
1584
- templates = list(fastapi_server._resource_manager.get_templates().values())
 
1585
  get_template = next((t for t in templates if "get_item_items" in t.name), None)
1586
 
1587
  assert get_template is not None, "GET /items/{item_id} template wasn't created"
@@ -1590,13 +1601,16 @@ class TestFastAPIDescriptionPropagation:
1590
  "Function docstring missing from ResourceTemplate"
1591
  )
1592
 
1593
- async def test_template_includes_path_parameter_description(self, fastapi_server):
 
 
1594
  """Test that a ResourceTemplate includes path parameter descriptions.
1595
 
1596
  Note: Currently, FastAPI parameter descriptions using Annotated[type, Field(description=...)]
1597
  are not properly propagated to the OpenAPI schema. The parameters appear but without the description.
1598
  """
1599
- templates = list(fastapi_server._resource_manager.get_templates().values())
 
1600
  get_template = next((t for t in templates if "get_item_items" in t.name), None)
1601
 
1602
  assert get_template is not None, "GET /items/{item_id} template wasn't created"
@@ -1610,13 +1624,16 @@ class TestFastAPIDescriptionPropagation:
1610
  "item_id parameter missing from ResourceTemplate description"
1611
  )
1612
 
1613
- async def test_template_includes_query_parameter_description(self, fastapi_server):
 
 
1614
  """Test that a ResourceTemplate includes query parameter descriptions.
1615
 
1616
  Note: Currently, FastAPI parameter descriptions using Annotated[type, Field(description=...)]
1617
  are not properly propagated to the OpenAPI schema. The parameters appear but without the description.
1618
  """
1619
- templates = list(fastapi_server._resource_manager.get_templates().values())
 
1620
  get_template = next((t for t in templates if "get_item_items" in t.name), None)
1621
 
1622
  assert get_template is not None, "GET /items/{item_id} template wasn't created"
@@ -1630,9 +1647,12 @@ class TestFastAPIDescriptionPropagation:
1630
  "fields parameter missing from ResourceTemplate description"
1631
  )
1632
 
1633
- async def test_template_parameter_schema_includes_description(self, fastapi_server):
 
 
1634
  """Test that a ResourceTemplate's parameter schema includes parameter descriptions."""
1635
- templates = list(fastapi_server._resource_manager.get_templates().values())
 
1636
  get_template = next((t for t in templates if "get_item_items" in t.name), None)
1637
 
1638
  assert get_template is not None, "GET /items/{item_id} template wasn't created"
@@ -1650,9 +1670,10 @@ class TestFastAPIDescriptionPropagation:
1650
  in get_template.parameters["properties"]["item_id"]["description"]
1651
  ), "Path parameter description incorrect in schema"
1652
 
1653
- async def test_tool_includes_function_docstring(self, fastapi_server):
1654
  """Test that a Tool includes the function docstring."""
1655
- tools = fastapi_server._tool_manager.list_tools()
 
1656
  create_tool = next(
1657
  (t for t in tools if "create_item_items_post" == t.name), None
1658
  )
@@ -1664,7 +1685,7 @@ class TestFastAPIDescriptionPropagation:
1664
  )
1665
 
1666
  async def test_tool_parameter_schema_includes_property_description(
1667
- self, fastapi_server
1668
  ):
1669
  """Test that a Tool's parameter schema includes property descriptions from request model.
1670
 
@@ -1672,7 +1693,8 @@ class TestFastAPIDescriptionPropagation:
1672
  may not be consistently propagated into the FastAPI OpenAPI schema and thus not into the tool's
1673
  parameter schema.
1674
  """
1675
- tools = fastapi_server._tool_manager.list_tools()
 
1676
  create_tool = next(
1677
  (t for t in tools if "create_item_items_post" == t.name), None
1678
  )
@@ -1686,7 +1708,7 @@ class TestFastAPIDescriptionPropagation:
1686
  )
1687
  # We don't test for the description field content as it may not be consistently propagated
1688
 
1689
- async def test_client_api_resource_description(self, fastapi_server):
1690
  """Test that Resource descriptions are accessible via the client API."""
1691
  async with Client(fastapi_server) as client:
1692
  resources = await client.list_resources()
@@ -1700,7 +1722,7 @@ class TestFastAPIDescriptionPropagation:
1700
  "Function docstring missing in Resource from client API"
1701
  )
1702
 
1703
- async def test_client_api_template_description(self, fastapi_server):
1704
  """Test that ResourceTemplate descriptions are accessible via the client API."""
1705
  async with Client(fastapi_server) as client:
1706
  templates = await client.list_resource_templates()
@@ -1716,7 +1738,7 @@ class TestFastAPIDescriptionPropagation:
1716
  "Function docstring missing in ResourceTemplate from client API"
1717
  )
1718
 
1719
- async def test_client_api_tool_description(self, fastapi_server):
1720
  """Test that Tool descriptions are accessible via the client API."""
1721
  async with Client(fastapi_server) as client:
1722
  tools = await client.list_tools()
@@ -1732,7 +1754,7 @@ class TestFastAPIDescriptionPropagation:
1732
  "Function docstring missing in Tool from client API"
1733
  )
1734
 
1735
- async def test_client_api_tool_parameter_schema(self, fastapi_server):
1736
  """Test that Tool parameter schemas are accessible via the client API."""
1737
  async with Client(fastapi_server) as client:
1738
  tools = await client.list_tools()
@@ -1755,11 +1777,9 @@ class TestFastAPIDescriptionPropagation:
1755
  class TestReprMethods:
1756
  """Tests for the custom __repr__ methods of OpenAPI objects."""
1757
 
1758
- async def test_openapi_tool_repr(
1759
- self, fastmcp_openapi_server_with_all_types: FastMCPOpenAPI
1760
- ):
1761
  """Test that OpenAPITool's __repr__ method works without recursion errors."""
1762
- tools = fastmcp_openapi_server_with_all_types._tool_manager._list_tools()
1763
  tool = next(iter(tools))
1764
 
1765
  # Verify repr doesn't cause recursion and contains expected elements
@@ -1769,13 +1789,10 @@ class TestReprMethods:
1769
  assert "method=" in tool_repr
1770
  assert "path=" in tool_repr
1771
 
1772
- async def test_openapi_resource_repr(
1773
- self, fastmcp_openapi_server_with_all_types: FastMCPOpenAPI
1774
- ):
1775
  """Test that OpenAPIResource's __repr__ method works without recursion errors."""
1776
- resources = list(
1777
- fastmcp_openapi_server_with_all_types._resource_manager.get_resources().values()
1778
- )
1779
  resource = next(iter(resources))
1780
 
1781
  # Verify repr doesn't cause recursion and contains expected elements
@@ -1786,12 +1803,13 @@ class TestReprMethods:
1786
  assert "path=" in resource_repr
1787
 
1788
  async def test_openapi_resource_template_repr(
1789
- self, fastmcp_openapi_server_with_all_types: FastMCPOpenAPI
1790
  ):
1791
  """Test that OpenAPIResourceTemplate's __repr__ method works without recursion errors."""
1792
- templates = list(
1793
- fastmcp_openapi_server_with_all_types._resource_manager.get_resource_templates().values()
1794
  )
 
1795
  template = next(iter(templates))
1796
 
1797
  # Verify repr doesn't cause recursion and contains expected elements
@@ -1836,7 +1854,7 @@ class TestEnumHandling:
1836
  )
1837
 
1838
  # Get the tools from the server
1839
- tools = server._tool_manager._list_tools()
1840
 
1841
  # Find the read_item tool
1842
  read_item_tool = next((t for t in tools if t.name == "read_item_items"), None)
@@ -1929,7 +1947,7 @@ class TestRouteMapWildcard:
1929
  )
1930
 
1931
  # All operations should be mapped to tools
1932
- tools = mcp._tool_manager._list_tools()
1933
  tool_names = {tool.name for tool in tools}
1934
 
1935
  # Check that all 4 operations became tools
@@ -2007,11 +2025,11 @@ class TestRouteMapTags:
2007
  )
2008
 
2009
  # Check that admin-tagged routes are tools
2010
- tools = server._tool_manager.get_tools()
2011
- tool_names = {t.name for t in tools.values()}
2012
 
2013
- resources = server._resource_manager.get_resources()
2014
- resource_names = {r.name for r in resources.values()}
2015
 
2016
  # Routes with "admin" tag should be tools
2017
  assert "createUser" in tool_names
@@ -2040,11 +2058,11 @@ class TestRouteMapTags:
2040
  )
2041
 
2042
  # Check that internal-tagged routes are excluded
2043
- resources = server._resource_manager.get_resources()
2044
- resource_names = {r.name for r in resources.values()}
2045
 
2046
- tools = server._tool_manager.get_tools()
2047
- tool_names = {t.name for t in tools.values()}
2048
 
2049
  # Internal-tagged route should be excluded
2050
  assert "getAdminStats" not in resource_names
@@ -2075,11 +2093,11 @@ class TestRouteMapTags:
2075
  route_maps=route_maps,
2076
  )
2077
 
2078
- tools = server._tool_manager.get_tools()
2079
- tool_names = {t.name for t in tools.values()}
2080
 
2081
- resources = server._resource_manager.get_resources()
2082
- resource_names = {r.name for r in resources.values()}
2083
 
2084
  # Only createUser has both "users" AND "admin" tags
2085
  assert "createUser" in tool_names
@@ -2110,11 +2128,11 @@ class TestRouteMapTags:
2110
  route_maps=route_maps,
2111
  )
2112
 
2113
- tools = server._tool_manager.get_tools()
2114
- tool_names = {t.name for t in tools.values()}
2115
 
2116
- resources = server._resource_manager.get_resources()
2117
- resource_names = {r.name for r in resources.values()}
2118
 
2119
  # Only getAdminStats matches both /admin/ pattern AND "admin" tag
2120
  assert "getAdminStats" in tool_names
@@ -2140,8 +2158,8 @@ class TestRouteMapTags:
2140
  route_maps=route_maps,
2141
  )
2142
 
2143
- tools = server._tool_manager.get_tools()
2144
- tool_names = {t.name for t in tools.values()}
2145
 
2146
  # All routes should be tools since empty tags matches everything
2147
  expected_tools = {
@@ -2246,18 +2264,18 @@ class TestMCPNames:
2246
  )
2247
 
2248
  # Check tools use custom names
2249
- tools = server._tool_manager._list_tools()
2250
  tool_names = {tool.name for tool in tools}
2251
  assert "admin_create_user" in tool_names
2252
 
2253
  # Check resource templates use custom names
2254
- templates = list(server._resource_manager.get_resource_templates().values())
2255
- template_names = {template.name for template in templates}
2256
  assert "user_detail" in template_names
2257
 
2258
  # Check resources use custom names
2259
- resources = list(server._resource_manager.get_resources().values())
2260
- resource_names = {resource.name for resource in resources}
2261
  assert "user_list" in resource_names
2262
 
2263
  async def test_mcp_names_fallback_to_operation_id_short(
@@ -2276,14 +2294,14 @@ class TestMCPNames:
2276
  route_maps=GET_ROUTE_MAPS,
2277
  )
2278
 
2279
- tools = server._tool_manager._list_tools()
2280
  tool_names = {tool.name for tool in tools}
2281
 
2282
- templates = list(server._resource_manager.get_resource_templates().values())
2283
- template_names = {template.name for template in templates}
2284
 
2285
- resources = list(server._resource_manager.get_resources().values())
2286
- resource_names = {resource.name for resource in resources}
2287
 
2288
  # Custom mapped name should be used
2289
  assert "custom_user_list" in resource_names
@@ -2300,9 +2318,11 @@ class TestMCPNames:
2300
  route_maps=GET_ROUTE_MAPS,
2301
  )
2302
 
2303
- resources = list(server._resource_manager.get_resources().values())
2304
  resource_names = {
2305
- resource.name for resource in resources if resource.name is not None
 
 
2306
  }
2307
 
2308
  # Special chars and spaces should be slugified
@@ -2330,14 +2350,14 @@ class TestMCPNames:
2330
  # Check all component types
2331
  all_names = []
2332
 
2333
- tools = server._tool_manager._list_tools()
2334
  all_names.extend(tool.name for tool in tools)
2335
 
2336
- resources = list(server._resource_manager.get_resources().values())
2337
- all_names.extend(resource.name for resource in resources)
2338
 
2339
- templates = list(server._resource_manager.get_resource_templates().values())
2340
- all_names.extend(template.name for template in templates)
2341
 
2342
  # All names should be 56 characters or less
2343
  for name in all_names:
@@ -2363,7 +2383,7 @@ class TestMCPNames:
2363
  mcp_names=mcp_names,
2364
  )
2365
 
2366
- tools = server._tool_manager._list_tools()
2367
  tool_names = {tool.name for tool in tools}
2368
  assert "openapi_user_list" in tool_names
2369
 
@@ -2395,7 +2415,7 @@ class TestMCPNames:
2395
  mcp_names=mcp_names,
2396
  )
2397
 
2398
- tools = server._tool_manager._list_tools()
2399
  tool_names = {tool.name for tool in tools}
2400
 
2401
  assert "fastapi_create_user" in tool_names
@@ -2419,9 +2439,11 @@ class TestMCPNames:
2419
  route_maps=GET_ROUTE_MAPS,
2420
  )
2421
 
2422
- resources = list(server._resource_manager.get_resources().values())
2423
  resource_names = {
2424
- resource.name for resource in resources if resource.name is not None
 
 
2425
  }
2426
 
2427
  # Find the resource that should have the custom name
@@ -2496,7 +2518,7 @@ class TestRouteMapMCPTags:
2496
  )
2497
 
2498
  # Get the POST tool
2499
- tools = server._tool_manager._list_tools()
2500
  create_user_tool = next((t for t in tools if "create_user" in t.name), None)
2501
 
2502
  assert create_user_tool is not None, "create_user tool not found"
@@ -2530,7 +2552,8 @@ class TestRouteMapMCPTags:
2530
  )
2531
 
2532
  # Get the resource
2533
- resources = list(server._resource_manager.get_resources().values())
 
2534
  get_users_resource = next((r for r in resources if "get_users" in r.name), None)
2535
 
2536
  assert get_users_resource is not None, "get_users resource not found"
@@ -2564,7 +2587,8 @@ class TestRouteMapMCPTags:
2564
  )
2565
 
2566
  # Get the resource template
2567
- templates = list(server._resource_manager.get_resource_templates().values())
 
2568
  get_user_template = next((t for t in templates if "get_user" in t.name), None)
2569
 
2570
  assert get_user_template is not None, "get_user template not found"
@@ -2610,21 +2634,23 @@ class TestRouteMapMCPTags:
2610
  )
2611
 
2612
  # Check tool tags
2613
- tools = server._tool_manager._list_tools()
2614
  create_tool = next((t for t in tools if "create_user" in t.name), None)
2615
  assert create_tool is not None
2616
  assert "write-operation" in create_tool.tags
2617
  assert "mutation" in create_tool.tags
2618
 
2619
  # Check resource template tags
2620
- templates = list(server._resource_manager.get_resource_templates().values())
 
2621
  detail_template = next((t for t in templates if "get_user" in t.name), None)
2622
  assert detail_template is not None
2623
  assert "detail" in detail_template.tags
2624
  assert "single-item" in detail_template.tags
2625
 
2626
  # Check resource tags
2627
- resources = list(server._resource_manager.get_resources().values())
 
2628
  list_resource = next((r for r in resources if "get_users" in r.name), None)
2629
  assert list_resource is not None
2630
  assert "list" in list_resource.tags
 
136
 
137
 
138
  @pytest.fixture
139
+ async def fastmcp_openapi_server(
140
  fastapi_app: FastAPI, api_client: httpx.AsyncClient
141
  ) -> FastMCPOpenAPI:
142
  openapi_spec = fastapi_app.openapi()
 
213
  assert len(await server.get_resources()) == 0
214
  assert len(await server.get_resource_templates()) == 0
215
 
216
+ async def test_list_tools(self, fastmcp_openapi_server: FastMCPOpenAPI):
 
 
217
  """
218
  By default, tools exclude GET methods
219
  """
220
+ async with Client(fastmcp_openapi_server) as client:
221
  tools = await client.list_tools()
222
  assert len(tools) == 2
223
 
 
252
 
253
  async def test_call_create_user_tool(
254
  self,
255
+ fastmcp_openapi_server: FastMCPOpenAPI,
256
  api_client,
257
  ):
258
  """
259
  The tool created by the OpenAPI server should be the same as the original
260
  """
261
+ async with Client(fastmcp_openapi_server) as client:
262
  tool_response = await client.call_tool(
263
  "create_user_users_post", {"name": "David", "active": False}
264
  )
 
272
  assert len(response.json()) == 4
273
 
274
  # Check that the user was created via MCP
275
+ async with Client(fastmcp_openapi_server) as client:
276
  user_response = await client.read_resource("resource://get_user_users/4")
277
  response_text = user_response[0].text # type: ignore[attr-defined]
278
  user = json.loads(response_text)
 
280
 
281
  async def test_call_update_user_name_tool(
282
  self,
283
+ fastmcp_openapi_server: FastMCPOpenAPI,
284
  api_client,
285
  ):
286
  """
287
  The tool created by the OpenAPI server should be the same as the original
288
  """
289
+ async with Client(fastmcp_openapi_server) as client:
290
  tool_response = await client.call_tool(
291
  "update_user_name_users",
292
  {"user_id": 1, "name": "XYZ"},
 
301
  assert expected_data in response.json()
302
 
303
  # Check that the user was updated via MCP
304
+ async with Client(fastmcp_openapi_server) as client:
305
  user_response = await client.read_resource("resource://get_user_users/1")
306
  response_text = user_response[0].text # type: ignore[attr-defined]
307
  user = json.loads(response_text)
 
333
 
334
 
335
  class TestResources:
336
+ async def test_list_resources(self, fastmcp_openapi_server: FastMCPOpenAPI):
 
 
337
  """
338
  By default, resources exclude GET methods without parameters
339
  """
340
+ async with Client(fastmcp_openapi_server) as client:
341
  resources = await client.list_resources()
342
  assert len(resources) == 4
343
  assert resources[0].uri == AnyUrl("resource://get_users_users_get")
 
345
 
346
  async def test_get_resource(
347
  self,
348
+ fastmcp_openapi_server: FastMCPOpenAPI,
349
  api_client,
350
  users_db: dict[int, User],
351
  ):
 
356
  json_users = TypeAdapter(list[User]).dump_python(
357
  sorted(users_db.values(), key=lambda x: x.id)
358
  )
359
+ async with Client(fastmcp_openapi_server) as client:
360
  resource_response = await client.read_resource(
361
  "resource://get_users_users_get"
362
  )
 
368
 
369
  async def test_get_bytes_resource(
370
  self,
371
+ fastmcp_openapi_server: FastMCPOpenAPI,
372
  api_client,
373
  ):
374
  """Test reading a resource that returns bytes."""
375
+ async with Client(fastmcp_openapi_server) as client:
376
  resource_response = await client.read_resource(
377
  "resource://ping_bytes_ping_bytes_get"
378
  )
 
381
 
382
  async def test_get_str_resource(
383
  self,
384
+ fastmcp_openapi_server: FastMCPOpenAPI,
385
  api_client,
386
  ):
387
  """Test reading a resource that returns a string."""
388
+ async with Client(fastmcp_openapi_server) as client:
389
  resource_response = await client.read_resource("resource://ping_ping_get")
390
  assert resource_response[0].text == "pong" # type: ignore[attr-defined]
391
 
392
 
393
  class TestResourceTemplates:
394
  async def test_list_resource_templates(
395
+ self, fastmcp_openapi_server: FastMCPOpenAPI
396
  ):
397
  """
398
  By default, resource templates exclude GET methods without parameters
399
  """
400
+ async with Client(fastmcp_openapi_server) as client:
401
  resource_templates = await client.list_resource_templates()
402
  assert len(resource_templates) == 2
403
  assert resource_templates[0].name == "get_user_users"
 
412
 
413
  async def test_get_resource_template(
414
  self,
415
+ fastmcp_openapi_server: FastMCPOpenAPI,
416
  api_client,
417
  users_db: dict[int, User],
418
  ):
 
420
  The resource template created by the OpenAPI server should be the same as the original
421
  """
422
  user_id = 2
423
+ async with Client(fastmcp_openapi_server) as client:
424
  resource_response = await client.read_resource(
425
  f"resource://get_user_users/{user_id}"
426
  )
 
433
 
434
  async def test_get_resource_template_multi_param(
435
  self,
436
+ fastmcp_openapi_server: FastMCPOpenAPI,
437
  api_client,
438
  users_db: dict[int, User],
439
  ):
 
442
  """
443
  user_id = 2
444
  is_active = True
445
+ async with Client(fastmcp_openapi_server) as client:
446
  resource_response = await client.read_resource(
447
  f"resource://get_user_active_state_users/{is_active}/{user_id}"
448
  )
 
455
 
456
 
457
  class TestPrompts:
458
+ async def test_list_prompts(self, fastmcp_openapi_server: FastMCPOpenAPI):
 
 
459
  """
460
  By default, there are no prompts.
461
  """
462
+ async with Client(fastmcp_openapi_server) as client:
463
  prompts = await client.list_prompts()
464
  assert len(prompts) == 0
465
 
 
468
  """Tests for transferring tags from OpenAPI routes to MCP objects."""
469
 
470
  async def test_tags_transferred_to_tools(
471
+ self, fastmcp_openapi_server: FastMCPOpenAPI
472
  ):
473
  """Test that tags from OpenAPI routes are correctly transferred to Tools."""
474
  # Get internal tools directly (not the public API which returns MCP.Content)
475
+ tools = await fastmcp_openapi_server._tool_manager._list_tools()
476
 
477
  # Find the create_user and update_user_name tools
478
  create_user_tool = next(
 
496
  assert len(update_user_tool.tags) == 2
497
 
498
  async def test_tags_transferred_to_resources(
499
+ self, fastmcp_openapi_server: FastMCPOpenAPI
500
  ):
501
  """Test that tags from OpenAPI routes are correctly transferred to Resources."""
502
  # Get internal resources directly
503
+ resources_dict = await fastmcp_openapi_server._resource_manager.get_resources()
504
+ resources = list(resources_dict.values())
 
505
 
506
  # Find the get_users resource
507
  get_users_resource = next(
 
516
  assert len(get_users_resource.tags) == 2
517
 
518
  async def test_tags_transferred_to_resource_templates(
519
+ self, fastmcp_openapi_server: FastMCPOpenAPI
520
  ):
521
  """Test that tags from OpenAPI routes are correctly transferred to ResourceTemplates."""
522
  # Get internal resource templates directly
523
+ templates_dict = (
524
+ await fastmcp_openapi_server._resource_manager.get_resource_templates()
525
  )
526
+ templates = list(templates_dict.values())
527
 
528
  # Find the get_user template
529
  get_user_template = next(
 
538
  assert len(get_user_template.tags) == 2
539
 
540
  async def test_tags_preserved_in_resources_created_from_templates(
541
+ self, fastmcp_openapi_server: FastMCPOpenAPI
542
  ):
543
  """Test that tags are preserved when creating resources from templates."""
544
  # Get internal resource templates directly
545
+ templates_dict = (
546
+ await fastmcp_openapi_server._resource_manager.get_resource_templates()
547
  )
548
+ templates = list(templates_dict.values())
549
 
550
  # Find the get_user template
551
  get_user_template = next(
 
1162
  return httpx.AsyncClient(transport=transport, base_url="http://test")
1163
 
1164
  @pytest.fixture
1165
+ async def simple_mcp_server(self, simple_openapi_spec, mock_client):
1166
  """Create a FastMCPOpenAPI server with the simple test spec."""
1167
  return FastMCPOpenAPI(
1168
  openapi_spec=simple_openapi_spec,
 
1174
  # --- RESOURCE TESTS ---
1175
 
1176
  async def test_resource_includes_route_description(
1177
+ self, simple_mcp_server: FastMCP
1178
  ):
1179
  """Test that a Resource includes the route description."""
1180
  resources = list(
1181
+ (await simple_mcp_server._resource_manager.get_resources()).values()
1182
  )
1183
  list_resource = next((r for r in resources if r.name == "listItems"), None)
1184
 
 
1188
  )
1189
 
1190
  async def test_resource_includes_response_description(
1191
+ self, simple_mcp_server: FastMCP
1192
  ):
1193
  """Test that a Resource includes the response description."""
1194
  resources = list(
1195
+ (await simple_mcp_server._resource_manager.get_resources()).values()
1196
  )
1197
  list_resource = next((r for r in resources if r.name == "listItems"), None)
1198
 
 
1202
  )
1203
 
1204
  async def test_resource_includes_response_model_fields(
1205
+ self, simple_mcp_server: FastMCP
1206
  ):
1207
  """Test that a Resource description includes response model field descriptions."""
1208
  resources = list(
1209
+ (await simple_mcp_server._resource_manager.get_resources()).values()
1210
  )
1211
  list_resource = next((r for r in resources if r.name == "listItems"), None)
1212
 
 
1225
  # --- RESOURCE TEMPLATE TESTS ---
1226
 
1227
  async def test_template_includes_route_description(
1228
+ self, simple_mcp_server: FastMCP
1229
  ):
1230
  """Test that a ResourceTemplate includes the route description."""
1231
+ templates_dict = (
1232
+ await simple_mcp_server._resource_manager.get_resource_templates()
1233
  )
1234
+ templates = list(templates_dict.values())
1235
  get_template = next((t for t in templates if t.name == "getItem"), None)
1236
 
1237
  assert get_template is not None, "getItem template wasn't created"
 
1240
  )
1241
 
1242
  async def test_template_includes_function_docstring(
1243
+ self, simple_mcp_server: FastMCP
1244
  ):
1245
  """Test that a ResourceTemplate includes the function docstring."""
1246
+ templates_dict = (
1247
+ await simple_mcp_server._resource_manager.get_resource_templates()
1248
  )
1249
+ templates = list(templates_dict.values())
1250
  get_template = next((t for t in templates if t.name == "getItem"), None)
1251
 
1252
  assert get_template is not None, "getItem template wasn't created"
 
1255
  )
1256
 
1257
  async def test_template_includes_path_parameter_description(
1258
+ self, simple_mcp_server: FastMCP
1259
  ):
1260
  """Test that a ResourceTemplate includes path parameter descriptions."""
1261
+ templates_dict = (
1262
+ await simple_mcp_server._resource_manager.get_resource_templates()
1263
  )
1264
+ templates = list(templates_dict.values())
1265
  get_template = next((t for t in templates if t.name == "getItem"), None)
1266
 
1267
  assert get_template is not None, "getItem template wasn't created"
 
1270
  )
1271
 
1272
  async def test_template_includes_query_parameter_description(
1273
+ self, simple_mcp_server: FastMCP
1274
  ):
1275
  """Test that a ResourceTemplate includes query parameter descriptions."""
1276
+ templates_dict = (
1277
+ await simple_mcp_server._resource_manager.get_resource_templates()
1278
  )
1279
+ templates = list(templates_dict.values())
1280
  get_template = next((t for t in templates if t.name == "getItem"), None)
1281
 
1282
  assert get_template is not None, "getItem template wasn't created"
 
1285
  )
1286
 
1287
  async def test_template_parameter_schema_includes_description(
1288
+ self, simple_mcp_server: FastMCP
1289
  ):
1290
  """Test that a ResourceTemplate's parameter schema includes parameter descriptions."""
1291
+ templates_dict = (
1292
+ await simple_mcp_server._resource_manager.get_resource_templates()
1293
  )
1294
+ templates = list(templates_dict.values())
1295
  get_template = next((t for t in templates if t.name == "getItem"), None)
1296
 
1297
  assert get_template is not None, "getItem template wasn't created"
 
1311
 
1312
  # --- TOOL TESTS ---
1313
 
1314
+ async def test_tool_includes_route_description(self, simple_mcp_server: FastMCP):
1315
  """Test that a Tool includes the route description."""
1316
+ tools_dict = await simple_mcp_server._tool_manager.get_tools()
1317
+ tools = list(tools_dict.values())
1318
  create_tool = next((t for t in tools if t.name == "createItem"), None)
1319
 
1320
  assert create_tool is not None, "createItem tool wasn't created"
 
1322
  "Route description missing from Tool"
1323
  )
1324
 
1325
+ async def test_tool_includes_function_docstring(self, simple_mcp_server: FastMCP):
1326
  """Test that a Tool includes the function docstring."""
1327
+ tools_dict = await simple_mcp_server._tool_manager.get_tools()
1328
+ tools = list(tools_dict.values())
1329
  create_tool = next((t for t in tools if t.name == "createItem"), None)
1330
 
1331
  assert create_tool is not None, "createItem tool wasn't created"
 
1335
  )
1336
 
1337
  async def test_tool_parameter_schema_includes_property_description(
1338
+ self, simple_mcp_server: FastMCP
1339
  ):
1340
  """Test that a Tool's parameter schema includes property descriptions from request model."""
1341
+ tools_dict = await simple_mcp_server._tool_manager.get_tools()
1342
+ tools = list(tools_dict.values())
1343
  create_tool = next((t for t in tools if t.name == "createItem"), None)
1344
 
1345
  assert create_tool is not None, "createItem tool wasn't created"
 
1359
 
1360
  # --- CLIENT API TESTS ---
1361
 
1362
+ async def test_client_api_resource_description(self, simple_mcp_server: FastMCP):
1363
  """Test that Resource descriptions are accessible via the client API."""
1364
+ async with Client(simple_mcp_server) as client:
1365
  resources = await client.list_resources()
1366
  list_resource = next((r for r in resources if r.name == "listItems"), None)
1367
 
 
1373
  "Route description missing in Resource from client API"
1374
  )
1375
 
1376
+ async def test_client_api_template_description(self, simple_mcp_server: FastMCP):
1377
  """Test that ResourceTemplate descriptions are accessible via the client API."""
1378
+ async with Client(simple_mcp_server) as client:
1379
  templates = await client.list_resource_templates()
1380
  get_template = next((t for t in templates if t.name == "getItem"), None)
1381
 
 
1387
  "Route description missing in ResourceTemplate from client API"
1388
  )
1389
 
1390
+ async def test_client_api_tool_description(self, simple_mcp_server: FastMCP):
1391
  """Test that Tool descriptions are accessible via the client API."""
1392
+ async with Client(simple_mcp_server) as client:
1393
  tools = await client.list_tools()
1394
  create_tool = next((t for t in tools if t.name == "createItem"), None)
1395
 
 
1401
  "Function docstring missing in Tool from client API"
1402
  )
1403
 
1404
+ async def test_client_api_tool_parameter_schema(self, simple_mcp_server: FastMCP):
1405
  """Test that Tool parameter schemas are accessible via the client API."""
1406
+ async with Client(simple_mcp_server) as client:
1407
  tools = await client.list_tools()
1408
  create_tool = next((t for t in tools if t.name == "createItem"), None)
1409
 
 
1536
 
1537
  # Debug: print all components created
1538
  print("\nDEBUG - Resources created:")
1539
+ resources_dict = await server._resource_manager.get_resources()
1540
+ for name, resource in resources_dict.items():
1541
  print(f" Resource: {name}, Name attribute: {resource.name}")
1542
 
1543
  print("\nDEBUG - Templates created:")
1544
+ templates_dict = await server._resource_manager.get_resource_templates()
1545
+ for name, template in templates_dict.items():
1546
  print(f" Template: {name}, Name attribute: {template.name}")
1547
 
1548
  print("\nDEBUG - Tools created:")
1549
+ tools = await server._tool_manager._list_tools()
1550
+ for tool in tools:
1551
  print(f" Tool: {tool.name}")
1552
 
1553
  return server
1554
 
1555
+ async def test_resource_includes_function_docstring(self, fastapi_server: FastMCP):
1556
  """Test that a Resource includes the function docstring."""
1557
+ resources_dict = await fastapi_server._resource_manager.get_resources()
1558
+ resources = list(resources_dict.values())
1559
 
1560
  # Now checking for the get_items operation ID rather than list_items
1561
  list_resource = next((r for r in resources if "items_get" in r.name), None)
 
1566
  "Function docstring missing from Resource"
1567
  )
1568
 
1569
+ async def test_resource_includes_response_model_fields(
1570
+ self, fastapi_server: FastMCP
1571
+ ):
1572
  """Test that a Resource description includes basic response information.
1573
 
1574
  Note: FastAPI doesn't reliably include Pydantic field descriptions in the OpenAPI schema,
1575
  so we can only check for basic response information being present.
1576
  """
1577
+ resources_dict = await fastapi_server._resource_manager.get_resources()
1578
+ resources = list(resources_dict.values())
1579
  list_resource = next((r for r in resources if "items_get" in r.name), None)
1580
 
1581
  assert list_resource is not None, "GET /items resource wasn't created"
 
1589
  # We've already verified in TestDescriptionPropagation that when descriptions
1590
  # are present in the OpenAPI schema, they are properly included in the component description
1591
 
1592
+ async def test_template_includes_function_docstring(self, fastapi_server: FastMCP):
1593
  """Test that a ResourceTemplate includes the function docstring."""
1594
+ templates_dict = await fastapi_server._resource_manager.get_resource_templates()
1595
+ templates = list(templates_dict.values())
1596
  get_template = next((t for t in templates if "get_item_items" in t.name), None)
1597
 
1598
  assert get_template is not None, "GET /items/{item_id} template wasn't created"
 
1601
  "Function docstring missing from ResourceTemplate"
1602
  )
1603
 
1604
+ async def test_template_includes_path_parameter_description(
1605
+ self, fastapi_server: FastMCP
1606
+ ):
1607
  """Test that a ResourceTemplate includes path parameter descriptions.
1608
 
1609
  Note: Currently, FastAPI parameter descriptions using Annotated[type, Field(description=...)]
1610
  are not properly propagated to the OpenAPI schema. The parameters appear but without the description.
1611
  """
1612
+ templates_dict = await fastapi_server._resource_manager.get_resource_templates()
1613
+ templates = list(templates_dict.values())
1614
  get_template = next((t for t in templates if "get_item_items" in t.name), None)
1615
 
1616
  assert get_template is not None, "GET /items/{item_id} template wasn't created"
 
1624
  "item_id parameter missing from ResourceTemplate description"
1625
  )
1626
 
1627
+ async def test_template_includes_query_parameter_description(
1628
+ self, fastapi_server: FastMCP
1629
+ ):
1630
  """Test that a ResourceTemplate includes query parameter descriptions.
1631
 
1632
  Note: Currently, FastAPI parameter descriptions using Annotated[type, Field(description=...)]
1633
  are not properly propagated to the OpenAPI schema. The parameters appear but without the description.
1634
  """
1635
+ templates_dict = await fastapi_server._resource_manager.get_resource_templates()
1636
+ templates = list(templates_dict.values())
1637
  get_template = next((t for t in templates if "get_item_items" in t.name), None)
1638
 
1639
  assert get_template is not None, "GET /items/{item_id} template wasn't created"
 
1647
  "fields parameter missing from ResourceTemplate description"
1648
  )
1649
 
1650
+ async def test_template_parameter_schema_includes_description(
1651
+ self, fastapi_server: FastMCP
1652
+ ):
1653
  """Test that a ResourceTemplate's parameter schema includes parameter descriptions."""
1654
+ templates_dict = await fastapi_server._resource_manager.get_resource_templates()
1655
+ templates = list(templates_dict.values())
1656
  get_template = next((t for t in templates if "get_item_items" in t.name), None)
1657
 
1658
  assert get_template is not None, "GET /items/{item_id} template wasn't created"
 
1670
  in get_template.parameters["properties"]["item_id"]["description"]
1671
  ), "Path parameter description incorrect in schema"
1672
 
1673
+ async def test_tool_includes_function_docstring(self, fastapi_server: FastMCP):
1674
  """Test that a Tool includes the function docstring."""
1675
+ tools_dict = await fastapi_server._tool_manager.get_tools()
1676
+ tools = list(tools_dict.values())
1677
  create_tool = next(
1678
  (t for t in tools if "create_item_items_post" == t.name), None
1679
  )
 
1685
  )
1686
 
1687
  async def test_tool_parameter_schema_includes_property_description(
1688
+ self, fastapi_server: FastMCP
1689
  ):
1690
  """Test that a Tool's parameter schema includes property descriptions from request model.
1691
 
 
1693
  may not be consistently propagated into the FastAPI OpenAPI schema and thus not into the tool's
1694
  parameter schema.
1695
  """
1696
+ tools_dict = await fastapi_server._tool_manager.get_tools()
1697
+ tools = list(tools_dict.values())
1698
  create_tool = next(
1699
  (t for t in tools if "create_item_items_post" == t.name), None
1700
  )
 
1708
  )
1709
  # We don't test for the description field content as it may not be consistently propagated
1710
 
1711
+ async def test_client_api_resource_description(self, fastapi_server: FastMCP):
1712
  """Test that Resource descriptions are accessible via the client API."""
1713
  async with Client(fastapi_server) as client:
1714
  resources = await client.list_resources()
 
1722
  "Function docstring missing in Resource from client API"
1723
  )
1724
 
1725
+ async def test_client_api_template_description(self, fastapi_server: FastMCP):
1726
  """Test that ResourceTemplate descriptions are accessible via the client API."""
1727
  async with Client(fastapi_server) as client:
1728
  templates = await client.list_resource_templates()
 
1738
  "Function docstring missing in ResourceTemplate from client API"
1739
  )
1740
 
1741
+ async def test_client_api_tool_description(self, fastapi_server: FastMCP):
1742
  """Test that Tool descriptions are accessible via the client API."""
1743
  async with Client(fastapi_server) as client:
1744
  tools = await client.list_tools()
 
1754
  "Function docstring missing in Tool from client API"
1755
  )
1756
 
1757
+ async def test_client_api_tool_parameter_schema(self, fastapi_server: FastMCP):
1758
  """Test that Tool parameter schemas are accessible via the client API."""
1759
  async with Client(fastapi_server) as client:
1760
  tools = await client.list_tools()
 
1777
  class TestReprMethods:
1778
  """Tests for the custom __repr__ methods of OpenAPI objects."""
1779
 
1780
+ async def test_openapi_tool_repr(self, fastmcp_openapi_server: FastMCPOpenAPI):
 
 
1781
  """Test that OpenAPITool's __repr__ method works without recursion errors."""
1782
+ tools = await fastmcp_openapi_server._tool_manager._list_tools()
1783
  tool = next(iter(tools))
1784
 
1785
  # Verify repr doesn't cause recursion and contains expected elements
 
1789
  assert "method=" in tool_repr
1790
  assert "path=" in tool_repr
1791
 
1792
+ async def test_openapi_resource_repr(self, fastmcp_openapi_server: FastMCPOpenAPI):
 
 
1793
  """Test that OpenAPIResource's __repr__ method works without recursion errors."""
1794
+ resources_dict = await fastmcp_openapi_server._resource_manager.get_resources()
1795
+ resources = list(resources_dict.values())
 
1796
  resource = next(iter(resources))
1797
 
1798
  # Verify repr doesn't cause recursion and contains expected elements
 
1803
  assert "path=" in resource_repr
1804
 
1805
  async def test_openapi_resource_template_repr(
1806
+ self, fastmcp_openapi_server: FastMCPOpenAPI
1807
  ):
1808
  """Test that OpenAPIResourceTemplate's __repr__ method works without recursion errors."""
1809
+ templates_dict = (
1810
+ await fastmcp_openapi_server._resource_manager.get_resource_templates()
1811
  )
1812
+ templates = list(templates_dict.values())
1813
  template = next(iter(templates))
1814
 
1815
  # Verify repr doesn't cause recursion and contains expected elements
 
1854
  )
1855
 
1856
  # Get the tools from the server
1857
+ tools = await server._tool_manager._list_tools()
1858
 
1859
  # Find the read_item tool
1860
  read_item_tool = next((t for t in tools if t.name == "read_item_items"), None)
 
1947
  )
1948
 
1949
  # All operations should be mapped to tools
1950
+ tools = await mcp._tool_manager._list_tools()
1951
  tool_names = {tool.name for tool in tools}
1952
 
1953
  # Check that all 4 operations became tools
 
2025
  )
2026
 
2027
  # Check that admin-tagged routes are tools
2028
+ tools_dict = await server._tool_manager.get_tools()
2029
+ tool_names = {t.name for t in tools_dict.values()}
2030
 
2031
+ resources_dict = await server._resource_manager.get_resources()
2032
+ resource_names = {r.name for r in resources_dict.values()}
2033
 
2034
  # Routes with "admin" tag should be tools
2035
  assert "createUser" in tool_names
 
2058
  )
2059
 
2060
  # Check that internal-tagged routes are excluded
2061
+ resources_dict = await server._resource_manager.get_resources()
2062
+ resource_names = {r.name for r in resources_dict.values()}
2063
 
2064
+ tools_dict = await server._tool_manager.get_tools()
2065
+ tool_names = {t.name for t in tools_dict.values()}
2066
 
2067
  # Internal-tagged route should be excluded
2068
  assert "getAdminStats" not in resource_names
 
2093
  route_maps=route_maps,
2094
  )
2095
 
2096
+ tools_dict = await server._tool_manager.get_tools()
2097
+ tool_names = {t.name for t in tools_dict.values()}
2098
 
2099
+ resources_dict = await server._resource_manager.get_resources()
2100
+ resource_names = {r.name for r in resources_dict.values()}
2101
 
2102
  # Only createUser has both "users" AND "admin" tags
2103
  assert "createUser" in tool_names
 
2128
  route_maps=route_maps,
2129
  )
2130
 
2131
+ tools_dict = await server._tool_manager.get_tools()
2132
+ tool_names = {t.name for t in tools_dict.values()}
2133
 
2134
+ resources_dict = await server._resource_manager.get_resources()
2135
+ resource_names = {r.name for r in resources_dict.values()}
2136
 
2137
  # Only getAdminStats matches both /admin/ pattern AND "admin" tag
2138
  assert "getAdminStats" in tool_names
 
2158
  route_maps=route_maps,
2159
  )
2160
 
2161
+ tools_dict = await server._tool_manager.get_tools()
2162
+ tool_names = {t.name for t in tools_dict.values()}
2163
 
2164
  # All routes should be tools since empty tags matches everything
2165
  expected_tools = {
 
2264
  )
2265
 
2266
  # Check tools use custom names
2267
+ tools = await server._tool_manager._list_tools()
2268
  tool_names = {tool.name for tool in tools}
2269
  assert "admin_create_user" in tool_names
2270
 
2271
  # Check resource templates use custom names
2272
+ templates_dict = await server._resource_manager.get_resource_templates()
2273
+ template_names = {template.name for template in templates_dict.values()}
2274
  assert "user_detail" in template_names
2275
 
2276
  # Check resources use custom names
2277
+ resources_dict = await server._resource_manager.get_resources()
2278
+ resource_names = {resource.name for resource in resources_dict.values()}
2279
  assert "user_list" in resource_names
2280
 
2281
  async def test_mcp_names_fallback_to_operation_id_short(
 
2294
  route_maps=GET_ROUTE_MAPS,
2295
  )
2296
 
2297
+ tools = await server._tool_manager._list_tools()
2298
  tool_names = {tool.name for tool in tools}
2299
 
2300
+ templates_dict = await server._resource_manager.get_resource_templates()
2301
+ template_names = {template.name for template in templates_dict.values()}
2302
 
2303
+ resources_dict = await server._resource_manager.get_resources()
2304
+ resource_names = {resource.name for resource in resources_dict.values()}
2305
 
2306
  # Custom mapped name should be used
2307
  assert "custom_user_list" in resource_names
 
2318
  route_maps=GET_ROUTE_MAPS,
2319
  )
2320
 
2321
+ resources_dict = await server._resource_manager.get_resources()
2322
  resource_names = {
2323
+ resource.name
2324
+ for resource in resources_dict.values()
2325
+ if resource.name is not None
2326
  }
2327
 
2328
  # Special chars and spaces should be slugified
 
2350
  # Check all component types
2351
  all_names = []
2352
 
2353
+ tools = await server._tool_manager._list_tools()
2354
  all_names.extend(tool.name for tool in tools)
2355
 
2356
+ resources_dict = await server._resource_manager.get_resources()
2357
+ all_names.extend(resource.name for resource in resources_dict.values())
2358
 
2359
+ templates_dict = await server._resource_manager.get_resource_templates()
2360
+ all_names.extend(template.name for template in templates_dict.values())
2361
 
2362
  # All names should be 56 characters or less
2363
  for name in all_names:
 
2383
  mcp_names=mcp_names,
2384
  )
2385
 
2386
+ tools = await server._tool_manager._list_tools()
2387
  tool_names = {tool.name for tool in tools}
2388
  assert "openapi_user_list" in tool_names
2389
 
 
2415
  mcp_names=mcp_names,
2416
  )
2417
 
2418
+ tools = await server._tool_manager._list_tools()
2419
  tool_names = {tool.name for tool in tools}
2420
 
2421
  assert "fastapi_create_user" in tool_names
 
2439
  route_maps=GET_ROUTE_MAPS,
2440
  )
2441
 
2442
+ resources_dict = await server._resource_manager.get_resources()
2443
  resource_names = {
2444
+ resource.name
2445
+ for resource in resources_dict.values()
2446
+ if resource.name is not None
2447
  }
2448
 
2449
  # Find the resource that should have the custom name
 
2518
  )
2519
 
2520
  # Get the POST tool
2521
+ tools = await server._tool_manager._list_tools()
2522
  create_user_tool = next((t for t in tools if "create_user" in t.name), None)
2523
 
2524
  assert create_user_tool is not None, "create_user tool not found"
 
2552
  )
2553
 
2554
  # Get the resource
2555
+ resources_dict = await server._resource_manager.get_resources()
2556
+ resources = list(resources_dict.values())
2557
  get_users_resource = next((r for r in resources if "get_users" in r.name), None)
2558
 
2559
  assert get_users_resource is not None, "get_users resource not found"
 
2587
  )
2588
 
2589
  # Get the resource template
2590
+ templates_dict = await server._resource_manager.get_resource_templates()
2591
+ templates = list(templates_dict.values())
2592
  get_user_template = next((t for t in templates if "get_user" in t.name), None)
2593
 
2594
  assert get_user_template is not None, "get_user template not found"
 
2634
  )
2635
 
2636
  # Check tool tags
2637
+ tools = await server._tool_manager._list_tools()
2638
  create_tool = next((t for t in tools if "create_user" in t.name), None)
2639
  assert create_tool is not None
2640
  assert "write-operation" in create_tool.tags
2641
  assert "mutation" in create_tool.tags
2642
 
2643
  # Check resource template tags
2644
+ templates_dict = await server._resource_manager.get_resource_templates()
2645
+ templates = list(templates_dict.values())
2646
  detail_template = next((t for t in templates if "get_user" in t.name), None)
2647
  assert detail_template is not None
2648
  assert "detail" in detail_template.tags
2649
  assert "single-item" in detail_template.tags
2650
 
2651
  # Check resource tags
2652
+ resources_dict = await server._resource_manager.get_resources()
2653
+ resources = list(resources_dict.values())
2654
  list_resource = next((r for r in resources if "get_users" in r.name), None)
2655
  assert list_resource is not None
2656
  assert "list" in list_resource.tags
tests/server/test_mount.py CHANGED
@@ -71,7 +71,8 @@ class TestBasicMount:
71
  # Mount without deprecated parameters
72
  main_app.mount(api_app, "api")
73
 
74
- async def test_mount_with_no_prefix(self):
 
75
  main_app = FastMCP("MainApp")
76
  sub_app = FastMCP("SubApp")
77
 
@@ -80,7 +81,7 @@ class TestBasicMount:
80
  return "This is from the sub app"
81
 
82
  # Mount with empty prefix but without deprecated separators
83
- main_app.mount(sub_app, prefix="")
84
 
85
  tools = await main_app.get_tools()
86
  # With empty prefix, the tool should keep its original name
 
71
  # Mount without deprecated parameters
72
  main_app.mount(api_app, "api")
73
 
74
+ @pytest.mark.parametrize("prefix", ["", None])
75
+ async def test_mount_with_no_prefix(self, prefix):
76
  main_app = FastMCP("MainApp")
77
  sub_app = FastMCP("SubApp")
78
 
 
81
  return "This is from the sub app"
82
 
83
  # Mount with empty prefix but without deprecated separators
84
+ main_app.mount(sub_app, prefix=prefix)
85
 
86
  tools = await main_app.get_tools()
87
  # With empty prefix, the tool should keep its original name
tests/server/test_resource_prefix_formats.py CHANGED
@@ -56,8 +56,8 @@ async def test_resource_prefix_format_in_import_server():
56
  await main_server_protocol.import_server(server, "sub")
57
 
58
  # Check that the resources are prefixed correctly
59
- path_resources = main_server_path._resource_manager.get_resources()
60
- protocol_resources = main_server_protocol._resource_manager.get_resources()
61
 
62
  # Path format should be resource://sub/test
63
  assert "resource://sub/test" in path_resources
 
56
  await main_server_protocol.import_server(server, "sub")
57
 
58
  # Check that the resources are prefixed correctly
59
+ path_resources = await main_server_path._resource_manager.get_resources()
60
+ protocol_resources = await main_server_protocol._resource_manager.get_resources()
61
 
62
  # Path format should be resource://sub/test
63
  assert "resource://sub/test" in path_resources
tests/server/test_tool_annotations.py CHANGED
@@ -22,7 +22,8 @@ async def test_tool_annotations_in_tool_manager():
22
  return message
23
 
24
  # Check internal tool objects directly
25
- tools = mcp._tool_manager._list_tools()
 
26
  assert len(tools) == 1
27
  assert tools[0].annotations is not None
28
  assert tools[0].annotations.title == "Echo Tool"
@@ -124,7 +125,8 @@ async def test_direct_tool_annotations_in_tool_manager():
124
  return {"modified": True, **data}
125
 
126
  # Check internal tool objects directly
127
- tools = mcp._tool_manager._list_tools()
 
128
  assert len(tools) == 1
129
  assert tools[0].annotations is not None
130
  assert tools[0].annotations.title == "Direct Tool"
@@ -183,7 +185,8 @@ async def test_add_tool_method_annotations():
183
  mcp.add_tool(tool)
184
 
185
  # Check internal tool objects directly
186
- tools = mcp._tool_manager._list_tools()
 
187
  assert len(tools) == 1
188
  assert tools[0].annotations is not None
189
  assert tools[0].annotations.title == "Create Item"
 
22
  return message
23
 
24
  # Check internal tool objects directly
25
+ tools_dict = await mcp._tool_manager.get_tools()
26
+ tools = list(tools_dict.values())
27
  assert len(tools) == 1
28
  assert tools[0].annotations is not None
29
  assert tools[0].annotations.title == "Echo Tool"
 
125
  return {"modified": True, **data}
126
 
127
  # Check internal tool objects directly
128
+ tools_dict = await mcp._tool_manager.get_tools()
129
+ tools = list(tools_dict.values())
130
  assert len(tools) == 1
131
  assert tools[0].annotations is not None
132
  assert tools[0].annotations.title == "Direct Tool"
 
185
  mcp.add_tool(tool)
186
 
187
  # Check internal tool objects directly
188
+ tools_dict = await mcp._tool_manager.get_tools()
189
+ tools = list(tools_dict.values())
190
  assert len(tools) == 1
191
  assert tools[0].annotations is not None
192
  assert tools[0].annotations.title == "Create Item"
tests/server/test_tool_exclude_args.py CHANGED
@@ -19,9 +19,10 @@ async def test_tool_exclude_args_in_tool_manager():
19
  pass
20
  return message
21
 
22
- tools = mcp._tool_manager._list_tools()
 
23
  assert len(tools) == 1
24
- assert "state" not in echo.parameters["properties"]
25
 
26
 
27
  async def test_tool_exclude_args_without_default_value_raises_error():
@@ -60,7 +61,8 @@ async def test_add_tool_method_exclude_args():
60
  mcp.add_tool(tool)
61
 
62
  # Check internal tool objects directly
63
- tools = mcp._tool_manager._list_tools()
 
64
  assert len(tools) == 1
65
  assert "state" not in tools[0].parameters["properties"]
66
 
 
19
  pass
20
  return message
21
 
22
+ tools_dict = await mcp._tool_manager.get_tools()
23
+ tools = list(tools_dict.values())
24
  assert len(tools) == 1
25
+ assert "state" not in tools[0].parameters["properties"]
26
 
27
 
28
  async def test_tool_exclude_args_without_default_value_raises_error():
 
61
  mcp.add_tool(tool)
62
 
63
  # Check internal tool objects directly
64
+ tools_dict = await mcp._tool_manager.get_tools()
65
+ tools = list(tools_dict.values())
66
  assert len(tools) == 1
67
  assert "state" not in tools[0].parameters["properties"]
68