Update resource manager

This commit is contained in:
Jeremiah Lowin 2025-06-19 12:22:14 -04:00
commit 35e13ef9bf
10 changed files with 353 additions and 218 deletions

View file

@ -65,14 +65,10 @@ class PromptManager:
child_results = await mounted.server._list_prompts()
else: # mode == "inventory"
# PATH 1: Use the manager-to-manager unfiltered path
child_results = await mounted.server._prompt_manager.get_prompts()
child_results = await mounted.server._prompt_manager._list_prompts()
# The combination logic is the same for both paths
child_dict = (
{p.key: p for p in child_results}
if isinstance(child_results, list)
else child_results
)
child_dict = {p.key: p for p in child_results}
if mounted.prefix:
for prompt in child_dict.values():
prefixed_prompt = prompt.with_key(
@ -110,7 +106,7 @@ class PromptManager:
"""
return await self._load_prompts(mode="inventory")
async def list_prompts(self) -> list[Prompt]:
async def _list_prompts(self) -> list[Prompt]:
"""
Lists all prompts, applying protocol filtering.
"""

View file

@ -1,9 +1,11 @@
"""Resource manager functionality."""
from __future__ import annotations
import inspect
import warnings
from collections.abc import Callable
from typing import Any
from typing import TYPE_CHECKING, Any, Literal
from pydantic import AnyUrl
@ -17,6 +19,9 @@ from fastmcp.resources.template import (
from fastmcp.settings import DuplicateBehavior
from fastmcp.utilities.logging import get_logger
if TYPE_CHECKING:
from fastmcp.server.server import MountedServer
logger = get_logger(__name__)
@ -38,6 +43,7 @@ class ResourceManager:
"""
self._resources: dict[str, Resource] = {}
self._templates: dict[str, ResourceTemplate] = {}
self._mounted_sources: list[MountedServer] = []
self.mask_error_details = mask_error_details or settings.mask_error_details
# Default to "warn" if None is provided
@ -51,6 +57,128 @@ class ResourceManager:
)
self.duplicate_behavior = duplicate_behavior
def mount(self, server: MountedServer) -> None:
"""Adds a mounted server as a source for resources and templates."""
self._mounted_sources.append(server)
async def get_resources(self) -> dict[str, Resource]:
"""Get all registered resources, keyed by URI."""
return await self._load_resources(mode="inventory")
async def get_resource_templates(self) -> dict[str, ResourceTemplate]:
"""Get all registered templates, keyed by URI template."""
return await self._load_resource_templates(mode="inventory")
async def _load_resources(
self, *, mode: Literal["inventory", "protocol"]
) -> dict[str, Resource]:
"""
The single, consolidated recursive method for fetching resources. The 'mode'
parameter determines the communication path.
- mode="inventory": Manager-to-manager path for complete, unfiltered inventory
- mode="protocol": Server-to-server path for filtered MCP requests
"""
all_resources: dict[str, Resource] = {}
for mounted in self._mounted_sources:
try:
if mode == "protocol":
# PATH 2: Use the server-to-server filtered path
child_resources_list = await mounted.server._list_resources()
child_resources = {
resource.key: resource for resource in child_resources_list
}
else: # mode == "inventory"
# PATH 1: Use the manager-to-manager unfiltered path
child_resources = (
await mounted.server._resource_manager.get_resources()
)
# Apply prefix if needed
if mounted.prefix:
from fastmcp.server.server import add_resource_prefix
for uri, resource in child_resources.items():
prefixed_uri = add_resource_prefix(
uri, mounted.prefix, mounted.resource_prefix_format
)
# Create a copy of the resource with the prefixed key
prefixed_resource = resource.with_key(prefixed_uri)
all_resources[prefixed_uri] = prefixed_resource
else:
all_resources.update(child_resources)
except Exception as e:
# Skip failed mounts silently, matches existing behavior
logger.warning(
f"Failed to get resources from mounted server '{mounted.prefix}': {e}"
)
continue
# Finally, add local resources, which always take precedence
all_resources.update(self._resources)
return all_resources
async def _load_resource_templates(
self, *, mode: Literal["inventory", "protocol"]
) -> dict[str, ResourceTemplate]:
"""
The single, consolidated recursive method for fetching templates. The 'mode'
parameter determines the communication path.
- mode="inventory": Manager-to-manager path for complete, unfiltered inventory
- mode="protocol": Server-to-server path for filtered MCP requests
"""
all_templates: dict[str, ResourceTemplate] = {}
for mounted in self._mounted_sources:
try:
if mode == "protocol":
# PATH 2: Use the server-to-server filtered path
child_templates = await mounted.server._list_resource_templates()
else: # mode == "inventory"
# PATH 1: Use the manager-to-manager unfiltered path
child_templates = await mounted.server._resource_manager._list_resource_templates()
child_dict = {template.key: template for template in child_templates}
# Apply prefix if needed
if mounted.prefix:
from fastmcp.server.server import add_resource_prefix
for uri_template, template in child_dict.items():
prefixed_uri_template = add_resource_prefix(
uri_template, mounted.prefix, mounted.resource_prefix_format
)
# Create a copy of the template with the prefixed key
prefixed_template = template.with_key(prefixed_uri_template)
all_templates[prefixed_uri_template] = prefixed_template
else:
all_templates.update(child_dict)
except Exception as e:
# Skip failed mounts silently, matches existing behavior
logger.warning(
f"Failed to get templates from mounted server '{mounted.prefix}': {e}"
)
continue
# Finally, add local templates, which always take precedence
all_templates.update(self._templates)
return all_templates
async def _list_resources(self) -> list[Resource]:
"""
Lists all resources, applying protocol filtering.
"""
resources_dict = await self._load_resources(mode="protocol")
return list(resources_dict.values())
async def _list_resource_templates(self) -> list[ResourceTemplate]:
"""
Lists all templates, applying protocol filtering.
"""
templates_dict = await self._load_resource_templates(mode="protocol")
return list(templates_dict.values())
def add_resource_or_template_from_fn(
self,
fn: Callable[..., Any],
@ -235,14 +363,21 @@ class ResourceManager:
self._templates[storage_key] = template
return template
def has_resource(self, uri: AnyUrl | str) -> bool:
async def has_resource(self, uri: AnyUrl | str) -> bool:
"""Check if a resource exists."""
uri_str = str(uri)
if uri_str in self._resources:
# First check concrete resources (local and mounted)
resources = await self.get_resources()
if uri_str in resources:
return True
for template_key in self._templates.keys():
# Then check templates (local and mounted) only if not found in concrete resources
templates = await self.get_resource_templates()
for template_key in templates.keys():
if match_uri_template(uri_str, template_key):
return True
return False
async def get_resource(self, uri: AnyUrl | str) -> Resource:
@ -257,12 +392,14 @@ class ResourceManager:
uri_str = str(uri)
logger.debug("Getting resource", extra={"uri": uri_str})
# First check concrete resources
if resource := self._resources.get(uri_str):
# First check concrete resources (local and mounted)
resources = await self.get_resources()
if resource := resources.get(uri_str):
return resource
# Then check templates - use the utility function to match against storage keys
for storage_key, template in self._templates.items():
# Then check templates (local and mounted) - use the utility function to match against storage keys
templates = await self.get_resource_templates()
for storage_key, template in templates.items():
# Try to match against the storage key (which might be a custom key)
if params := match_uri_template(uri_str, storage_key):
try:
@ -289,31 +426,89 @@ class ResourceManager:
raise NotFoundError(f"Unknown resource: {uri_str}")
async def read_resource(self, uri: AnyUrl | str) -> str | bytes:
"""Read a resource contents."""
resource = await self.get_resource(uri)
"""
Internal API for servers: Finds and reads a resource, respecting the
filtered protocol path.
"""
uri_str = str(uri)
try:
return await resource.read()
# 1. Check local resources first. The server will have already applied its filter.
if uri_str in self._resources:
resource = await self.get_resource(uri_str)
if not resource:
raise NotFoundError(f"Resource {uri_str!r} not found")
# raise ResourceErrors as-is
except ResourceError as e:
logger.error(f"Error reading resource {uri!r}: {e}")
raise e
try:
return await resource.read()
# Handle other exceptions
except Exception as e:
logger.error(f"Error reading resource {uri!r}: {e}")
if self.mask_error_details:
# Mask internal details
raise ResourceError(f"Error reading resource {uri!r}") from e
else:
# Include original error details
raise ResourceError(f"Error reading resource {uri!r}: {e}") from e
# raise ResourceErrors as-is
except ResourceError as e:
logger.exception(f"Error reading resource {uri_str!r}: {e}")
raise e
def get_resources(self) -> dict[str, Resource]:
"""Get all registered resources, keyed by URI."""
return self._resources
# Handle other exceptions
except Exception as e:
logger.exception(f"Error reading resource {uri_str!r}: {e}")
if self.mask_error_details:
# Mask internal details
raise ResourceError(f"Error reading resource {uri_str!r}") from e
else:
# Include original error details
raise ResourceError(
f"Error reading resource {uri_str!r}: {e}"
) from e
def get_templates(self) -> dict[str, ResourceTemplate]:
"""Get all registered templates, keyed by URI template."""
return self._templates
# 1b. Check local templates if not found in concrete resources
for template in self._templates.values():
if params := match_uri_template(uri_str, template.uri_template):
try:
resource = await template.create_resource(uri_str, params=params)
return await resource.read()
except ResourceError as e:
logger.exception(
f"Error reading resource from template {uri_str!r}: {e}"
)
raise e
except Exception as e:
logger.exception(
f"Error reading resource from template {uri_str!r}: {e}"
)
if self.mask_error_details:
raise ResourceError(
f"Error reading resource from template {uri_str!r}"
) from e
else:
raise ResourceError(
f"Error reading resource from template {uri_str!r}: {e}"
) from e
# 2. Check mounted servers using the filtered protocol path.
from fastmcp.server.server import has_resource_prefix, remove_resource_prefix
for mounted in reversed(self._mounted_sources):
resource_uri = uri_str
try:
if mounted.prefix:
# If server has a prefix, check if URI matches and strip prefix
if has_resource_prefix(
resource_uri,
mounted.prefix,
mounted.resource_prefix_format,
):
resource_uri = remove_resource_prefix(
resource_uri,
mounted.prefix,
mounted.resource_prefix_format,
)
else:
continue
result = await mounted.server._read_resource(resource_uri)
# Extract content from the first ReadResourceContents
if result and len(result) > 0:
return result[0].content
raise NotFoundError(f"Resource {uri_str!r} returned empty content")
except NotFoundError:
continue
raise NotFoundError(f"Resource {uri_str!r} not found.")

View file

@ -351,8 +351,7 @@ class FastMCP(Generic[LifespanResultT]):
async def get_resources(self) -> dict[str, Resource]:
"""Get all registered resources, indexed by registered key."""
resources = await self._list_resources(apply_middleware=False)
return {resource.key: resource for resource in resources}
return await self._resource_manager.get_resources()
async def get_resource(self, key: str) -> Resource:
resources = await self.get_resources()
@ -362,8 +361,7 @@ class FastMCP(Generic[LifespanResultT]):
async def get_resource_templates(self) -> dict[str, ResourceTemplate]:
"""Get all registered resource templates, indexed by registered key."""
templates = await self._list_resource_templates(apply_middleware=False)
return {template.key: template for template in templates}
return await self._resource_manager.get_resource_templates()
async def get_resource_template(self, key: str) -> ResourceTemplate:
templates = await self.get_resource_templates()
@ -444,7 +442,7 @@ class FastMCP(Generic[LifespanResultT]):
async def _handler(
context: MiddlewareContext[mcp.types.ListToolsRequest],
) -> list[Tool]:
tools = await self._tool_manager.list_tools()
tools = await self._tool_manager._list_tools() # type: ignore[reportPrivateUsage]
mcp_tools: list[Tool] = []
for tool in tools:
@ -470,12 +468,12 @@ class FastMCP(Generic[LifespanResultT]):
logger.debug("Handler called: list_resources")
with fastmcp.server.context.Context(fastmcp=self):
resources = await self._middleware_list_resources()
resources = await self._list_resources()
return [
resource.to_mcp_resource(uri=resource.key) for resource in resources
]
async def _middleware_list_resources(self) -> list[Resource]:
async def _list_resources(self) -> list[Resource]:
"""
List all available resources, in the format expected by the low-level MCP
server.
@ -485,7 +483,7 @@ class FastMCP(Generic[LifespanResultT]):
async def _handler(
context: MiddlewareContext[dict[str, Any]],
) -> list[Resource]:
resources = await self._list_resources()
resources = await self._resource_manager._list_resources() # type: ignore[reportPrivateUsage]
mcp_resources: list[Resource] = []
for resource in resources:
@ -507,62 +505,17 @@ class FastMCP(Generic[LifespanResultT]):
# Apply the middleware chain.
return await self._apply_middleware(mw_context, _handler)
async def _list_resources(self, apply_middleware: bool = True) -> list[Resource]:
"""
List all available resources.
"""
if (resources := self._cache.get("resources")) is self._cache.NOT_FOUND:
resources: dict[str, Resource] = {}
# iterate such that new mounts overwrite older ones
for mounted_server in self._mounted_servers:
try:
if apply_middleware:
server_resources = (
await mounted_server.server._middleware_list_resources()
)
else:
server_resources = await mounted_server.server._list_resources()
# Apply prefix to each resource key if prefix exists
if mounted_server.prefix:
for resource in server_resources:
resource = resource.with_key(
add_resource_prefix(
resource.key,
mounted_server.prefix,
self.resource_prefix_format,
)
)
resources[resource.key] = resource
else:
resources.update(
{resource.key: resource for resource in server_resources}
)
except Exception as e:
logger.warning(
f"Failed to get resources from mounted server '{mounted_server.prefix}': {e}"
)
continue
(
local_resources,
_,
) = await self._resource_manager.get_resources_and_templates()
resources.update(local_resources)
self._cache.set("resources", resources)
return list(resources.values())
async def _mcp_list_resource_templates(self) -> list[MCPResourceTemplate]:
logger.debug("Handler called: list_resource_templates")
with fastmcp.server.context.Context(fastmcp=self):
templates = await self._middleware_list_resource_templates()
templates = await self._list_resource_templates()
return [
template.to_mcp_template(uriTemplate=template.key)
for template in templates
]
async def _middleware_list_resource_templates(self) -> list[ResourceTemplate]:
async def _list_resource_templates(self) -> list[ResourceTemplate]:
"""
List all available resource templates, in the format expected by the low-level MCP
server.
@ -572,7 +525,7 @@ class FastMCP(Generic[LifespanResultT]):
async def _handler(
context: MiddlewareContext[dict[str, Any]],
) -> list[ResourceTemplate]:
templates = await self._list_resource_templates()
templates = await self._resource_manager._list_resource_templates()
mcp_templates: list[ResourceTemplate] = []
for template in templates:
@ -594,56 +547,6 @@ class FastMCP(Generic[LifespanResultT]):
# Apply the middleware chain.
return await self._apply_middleware(mw_context, _handler)
async def _list_resource_templates(
self, apply_middleware: bool = True
) -> list[ResourceTemplate]:
"""
List all available resource templates.
"""
if (
templates := self._cache.get("resource_templates")
) is self._cache.NOT_FOUND:
templates: dict[str, ResourceTemplate] = {}
# iterate such that new mounts overwrite older ones
for mounted_server in self._mounted_servers:
try:
if apply_middleware:
server_templates = await mounted_server.server._middleware_list_resource_templates()
else:
server_templates = (
await mounted_server.server._list_resource_templates()
)
# Apply prefix to each template key if prefix exists
if mounted_server.prefix:
for template in server_templates:
template = template.with_key(
add_resource_prefix(
template.key,
mounted_server.prefix,
self.resource_prefix_format,
)
)
templates[template.key] = template
else:
templates.update(
{template.key: template for template in server_templates}
)
except Exception as e:
logger.warning(
"Failed to get resource templates from mounted server "
f"'{mounted_server.prefix}': {e}"
)
continue
(
_,
local_templates,
) = await self._resource_manager.get_resources_and_templates()
templates.update(local_templates)
self._cache.set("resource_templates", templates)
return list(templates.values())
async def _mcp_list_prompts(self) -> list[MCPPrompt]:
logger.debug("Handler called: list_prompts")
@ -661,7 +564,7 @@ class FastMCP(Generic[LifespanResultT]):
async def _handler(
context: MiddlewareContext[mcp.types.ListPromptsRequest],
) -> list[Prompt]:
prompts = await self._prompt_manager.list_prompts()
prompts = await self._prompt_manager._list_prompts() # type: ignore[reportPrivateUsage]
mcp_prompts: list[Prompt] = []
for prompt in prompts:
@ -743,7 +646,7 @@ class FastMCP(Generic[LifespanResultT]):
with fastmcp.server.context.Context(fastmcp=self):
try:
return await self._middleware_read_resource(uri)
return await self._read_resource(uri)
except DisabledError:
# convert to NotFoundError to avoid leaking resource presence
raise NotFoundError(f"Unknown resource: {str(uri)!r}")
@ -751,25 +654,28 @@ class FastMCP(Generic[LifespanResultT]):
# standardize NotFound message
raise NotFoundError(f"Unknown resource: {str(uri)!r}")
async def _middleware_read_resource(
self,
uri: AnyUrl | str,
) -> list[ReadResourceContents]:
async def _read_resource(self, uri: AnyUrl | str) -> list[ReadResourceContents]:
"""
Read a resource with middleware.
Applies this server's middleware and delegates the filtered call to the manager.
"""
async def _handler(
context: MiddlewareContext[mcp.types.ReadResourceRequestParams],
) -> list[ReadResourceContents]:
return await self._read_resource(
uri=context.message.uri,
)
resource = await self._resource_manager.get_resource(context.message.uri)
if not self._should_enable_component(resource):
raise NotFoundError(f"Unknown resource: {str(context.message.uri)!r}")
content = await self._resource_manager.read_resource(context.message.uri)
return [
ReadResourceContents(
content=content,
mime_type=resource.mime_type,
)
]
# Convert string URI to AnyUrl if needed
if isinstance(uri, str):
from pydantic import AnyUrl
uri_param = AnyUrl(uri)
else:
uri_param = uri
@ -783,25 +689,6 @@ class FastMCP(Generic[LifespanResultT]):
)
return await self._apply_middleware(mw_context, _handler)
async def _read_resource(self, uri: AnyUrl | str) -> list[ReadResourceContents]:
"""
Read a resource by URI, in the format expected by the low-level MCP
server.
"""
if await self._resource_manager.has_resource(uri):
resource = await self._resource_manager.get_resource(uri)
if not self._should_enable_component(resource):
raise DisabledError(f"Resource {str(uri)!r} is disabled")
content = await self._resource_manager.read_resource(uri)
return [
ReadResourceContents(
content=content,
mime_type=resource.mime_type,
)
]
else:
raise NotFoundError(f"Unknown resource: {uri}")
async def _mcp_get_prompt(
self, name: str, arguments: dict[str, Any] | None = None
) -> GetPromptResult:
@ -1681,7 +1568,11 @@ class FastMCP(Generic[LifespanResultT]):
server = FastMCPProxy(Client(transport=FastMCPTransport(server)))
# Delegate mounting to all three managers
mounted_server = MountedServer(prefix=prefix, server=server)
mounted_server = MountedServer(
prefix=prefix,
server=server,
resource_prefix_format=self.resource_prefix_format,
)
self._tool_manager.mount(mounted_server)
self._resource_manager.mount(mounted_server)
self._prompt_manager.mount(mounted_server)
@ -1979,6 +1870,7 @@ class FastMCP(Generic[LifespanResultT]):
class MountedServer:
prefix: str | None
server: FastMCP[Any]
resource_prefix_format: Literal["protocol", "path"] | None = None
def add_resource_prefix(

View file

@ -66,14 +66,10 @@ class ToolManager:
child_results = await mounted.server._list_tools()
else: # mode == "inventory"
# PATH 1: Use the manager-to-manager unfiltered path
child_results = await mounted.server._tool_manager.get_tools()
child_results = await mounted.server._tool_manager._list_tools()
# The combination logic is the same for both paths
child_dict = (
{t.key: t for t in child_results}
if isinstance(child_results, list)
else child_results
)
child_dict = {t.key: t for t in child_results}
if mounted.prefix:
for tool in child_dict.values():
prefixed_tool = tool.with_key(f"{mounted.prefix}_{tool.key}")
@ -109,7 +105,7 @@ class ToolManager:
"""
return await self._load_tools(mode="inventory")
async def list_tools(self) -> list[Tool]:
async def _list_tools(self) -> list[Tool]:
"""
Lists all tools, applying protocol filtering.
"""

View file

@ -180,7 +180,7 @@ class TestResourceManager:
assert "Template already exists" in caplog.text
# Should have the template
assert manager.get_templates() == {"test://{id}": template}
assert manager.get_resource_templates() == {"test://{id}": template}
def test_error_on_duplicate_templates(self):
"""Test error on duplicate templates."""
@ -226,7 +226,7 @@ class TestResourceManager:
manager.add_template(template2)
# Should have replaced with the new template
templates = list(manager.get_templates().values())
templates = list(manager.get_resource_templates().values())
assert len(templates) == 1
assert templates[0].name == "replacement"
@ -256,7 +256,7 @@ class TestResourceManager:
result = manager.add_template(template2)
# Should keep the original
templates = list(manager.get_templates().values())
templates = list(manager.get_resource_templates().values())
assert len(templates) == 1
assert templates[0].name == "original"
# Result should be the original template
@ -379,7 +379,7 @@ class TestResourceTags:
)
manager.add_template(template)
templates = list(manager.get_templates().values())
templates = list(manager.get_resource_templates().values())
assert len(templates) == 1
assert templates[0].tags == {"users", "template", "data"}

View file

@ -172,6 +172,18 @@ class TestMiddlewareHooks:
assert recording_middleware.assert_called(hook="on_request", times=1)
assert recording_middleware.assert_called(hook="on_read_resource", times=1)
async def test_read_resource_template(
self, mcp_server: FastMCP, recording_middleware: RecordingMiddleware
):
async with Client(mcp_server) as client:
await client.read_resource("resource://test-template/1")
assert recording_middleware.assert_called(times=3)
assert recording_middleware.assert_called(method="resources/read", times=3)
assert recording_middleware.assert_called(hook="on_message", times=1)
assert recording_middleware.assert_called(hook="on_request", times=1)
assert recording_middleware.assert_called(hook="on_read_resource", times=1)
async def test_get_prompt(
self, mcp_server: FastMCP, recording_middleware: RecordingMiddleware
):
@ -367,6 +379,50 @@ class TestNestedMiddlewareHooks:
assert nested_middleware.assert_called(hook="on_request", times=1)
assert nested_middleware.assert_called(hook="on_read_resource", times=1)
async def test_read_resource_template_on_parent_server(
self,
mcp_server: FastMCP,
nested_mcp_server: FastMCP,
recording_middleware: RecordingMiddleware,
nested_middleware: RecordingMiddleware,
):
mcp_server.mount(nested_mcp_server, prefix="nested")
async with Client(mcp_server) as client:
await client.read_resource("resource://test-template/1")
assert recording_middleware.assert_called(times=3)
assert recording_middleware.assert_called(method="resources/read", times=3)
assert recording_middleware.assert_called(hook="on_message", times=1)
assert recording_middleware.assert_called(hook="on_request", times=1)
assert recording_middleware.assert_called(hook="on_read_resource", times=1)
assert nested_middleware.assert_called(times=0)
async def test_read_resource_template_on_nested_server(
self,
mcp_server: FastMCP,
nested_mcp_server: FastMCP,
recording_middleware: RecordingMiddleware,
nested_middleware: RecordingMiddleware,
):
mcp_server.mount(nested_mcp_server, prefix="nested")
async with Client(mcp_server) as client:
await client.read_resource("resource://nested/test-template/1")
assert recording_middleware.assert_called(times=3)
assert recording_middleware.assert_called(method="resources/read", times=3)
assert recording_middleware.assert_called(hook="on_message", times=1)
assert recording_middleware.assert_called(hook="on_request", times=1)
assert recording_middleware.assert_called(hook="on_read_resource", times=1)
assert nested_middleware.assert_called(times=3)
assert nested_middleware.assert_called(method="resources/read", times=3)
assert nested_middleware.assert_called(hook="on_message", times=1)
assert nested_middleware.assert_called(hook="on_request", times=1)
assert nested_middleware.assert_called(hook="on_read_resource", times=1)
async def test_get_prompt_on_parent_server(
self,
mcp_server: FastMCP,

View file

@ -478,7 +478,7 @@ class TestTagTransfer:
):
"""Test that tags from OpenAPI routes are correctly transferred to Tools."""
# Get internal tools directly (not the public API which returns MCP.Content)
tools = fastmcp_openapi_server_with_all_types._tool_manager.list_tools()
tools = fastmcp_openapi_server_with_all_types._tool_manager._list_tools()
# Find the create_user and update_user_name tools
create_user_tool = next(
@ -528,7 +528,7 @@ class TestTagTransfer:
"""Test that tags from OpenAPI routes are correctly transferred to ResourceTemplates."""
# Get internal resource templates directly
templates = list(
fastmcp_openapi_server_with_all_types._resource_manager.get_templates().values()
fastmcp_openapi_server_with_all_types._resource_manager.get_resource_templates().values()
)
# Find the get_user template
@ -549,7 +549,7 @@ class TestTagTransfer:
"""Test that tags are preserved when creating resources from templates."""
# Get internal resource templates directly
templates = list(
fastmcp_openapi_server_with_all_types._resource_manager.get_templates().values()
fastmcp_openapi_server_with_all_types._resource_manager.get_resource_templates().values()
)
# Find the get_user template
@ -1537,11 +1537,11 @@ class TestFastAPIDescriptionPropagation:
print(f" Resource: {name}, Name attribute: {resource.name}")
print("\nDEBUG - Templates created:")
for name, template in server._resource_manager.get_templates().items():
for name, template in server._resource_manager.get_resource_templates().items():
print(f" Template: {name}, Name attribute: {template.name}")
print("\nDEBUG - Tools created:")
for tool in server._tool_manager.list_tools():
for tool in server._tool_manager._list_tools():
print(f" Tool: {tool.name}")
return server
@ -1759,7 +1759,7 @@ class TestReprMethods:
self, fastmcp_openapi_server_with_all_types: FastMCPOpenAPI
):
"""Test that OpenAPITool's __repr__ method works without recursion errors."""
tools = fastmcp_openapi_server_with_all_types._tool_manager.list_tools()
tools = fastmcp_openapi_server_with_all_types._tool_manager._list_tools()
tool = next(iter(tools))
# Verify repr doesn't cause recursion and contains expected elements
@ -1790,7 +1790,7 @@ class TestReprMethods:
):
"""Test that OpenAPIResourceTemplate's __repr__ method works without recursion errors."""
templates = list(
fastmcp_openapi_server_with_all_types._resource_manager.get_templates().values()
fastmcp_openapi_server_with_all_types._resource_manager.get_resource_templates().values()
)
template = next(iter(templates))
@ -1836,7 +1836,7 @@ class TestEnumHandling:
)
# Get the tools from the server
tools = server._tool_manager.list_tools()
tools = server._tool_manager._list_tools()
# Find the read_item tool
read_item_tool = next((t for t in tools if t.name == "read_item_items"), None)
@ -1929,7 +1929,7 @@ class TestRouteMapWildcard:
)
# All operations should be mapped to tools
tools = mcp._tool_manager.list_tools()
tools = mcp._tool_manager._list_tools()
tool_names = {tool.name for tool in tools}
# Check that all 4 operations became tools
@ -2246,12 +2246,12 @@ class TestMCPNames:
)
# Check tools use custom names
tools = server._tool_manager.list_tools()
tools = server._tool_manager._list_tools()
tool_names = {tool.name for tool in tools}
assert "admin_create_user" in tool_names
# Check resource templates use custom names
templates = list(server._resource_manager.get_templates().values())
templates = list(server._resource_manager.get_resource_templates().values())
template_names = {template.name for template in templates}
assert "user_detail" in template_names
@ -2276,10 +2276,10 @@ class TestMCPNames:
route_maps=GET_ROUTE_MAPS,
)
tools = server._tool_manager.list_tools()
tools = server._tool_manager._list_tools()
tool_names = {tool.name for tool in tools}
templates = list(server._resource_manager.get_templates().values())
templates = list(server._resource_manager.get_resource_templates().values())
template_names = {template.name for template in templates}
resources = list(server._resource_manager.get_resources().values())
@ -2330,13 +2330,13 @@ class TestMCPNames:
# Check all component types
all_names = []
tools = server._tool_manager.list_tools()
tools = server._tool_manager._list_tools()
all_names.extend(tool.name for tool in tools)
resources = list(server._resource_manager.get_resources().values())
all_names.extend(resource.name for resource in resources)
templates = list(server._resource_manager.get_templates().values())
templates = list(server._resource_manager.get_resource_templates().values())
all_names.extend(template.name for template in templates)
# All names should be 56 characters or less
@ -2363,7 +2363,7 @@ class TestMCPNames:
mcp_names=mcp_names,
)
tools = server._tool_manager.list_tools()
tools = server._tool_manager._list_tools()
tool_names = {tool.name for tool in tools}
assert "openapi_user_list" in tool_names
@ -2395,7 +2395,7 @@ class TestMCPNames:
mcp_names=mcp_names,
)
tools = server._tool_manager.list_tools()
tools = server._tool_manager._list_tools()
tool_names = {tool.name for tool in tools}
assert "fastapi_create_user" in tool_names
@ -2496,7 +2496,7 @@ class TestRouteMapMCPTags:
)
# Get the POST tool
tools = server._tool_manager.list_tools()
tools = server._tool_manager._list_tools()
create_user_tool = next((t for t in tools if "create_user" in t.name), None)
assert create_user_tool is not None, "create_user tool not found"
@ -2564,7 +2564,7 @@ class TestRouteMapMCPTags:
)
# Get the resource template
templates = list(server._resource_manager.get_templates().values())
templates = list(server._resource_manager.get_resource_templates().values())
get_user_template = next((t for t in templates if "get_user" in t.name), None)
assert get_user_template is not None, "get_user template not found"
@ -2610,14 +2610,14 @@ class TestRouteMapMCPTags:
)
# Check tool tags
tools = server._tool_manager.list_tools()
tools = server._tool_manager._list_tools()
create_tool = next((t for t in tools if "create_user" in t.name), None)
assert create_tool is not None
assert "write-operation" in create_tool.tags
assert "mutation" in create_tool.tags
# Check resource template tags
templates = list(server._resource_manager.get_templates().values())
templates = list(server._resource_manager.get_resource_templates().values())
detail_template = next((t for t in templates if "get_user" in t.name), None)
assert detail_template is not None
assert "detail" in detail_template.tags

View file

@ -282,7 +282,7 @@ class TestToolDecorator:
return x * 2
# Verify the tags were set correctly
tools = mcp._tool_manager.list_tools()
tools = await mcp._tool_manager._list_tools()
assert len(tools) == 1
assert tools[0].tags == {"example", "test-tag"}

View file

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

View file

@ -19,7 +19,7 @@ async def test_tool_exclude_args_in_tool_manager():
pass
return message
tools = mcp._tool_manager.list_tools()
tools = mcp._tool_manager._list_tools()
assert len(tools) == 1
assert "state" not in echo.parameters["properties"]
@ -60,7 +60,7 @@ async def test_add_tool_method_exclude_args():
mcp.add_tool(tool)
# Check internal tool objects directly
tools = mcp._tool_manager.list_tools()
tools = mcp._tool_manager._list_tools()
assert len(tools) == 1
assert "state" not in tools[0].parameters["properties"]