"""Resource manager functionality."""
import inspect
import warnings
from collections.abc import Callable
from typing import Any
from pydantic import AnyUrl
from fastmcp import settings
from fastmcp.exceptions import NotFoundError, ResourceError
from fastmcp.resources.resource import Resource
from fastmcp.resources.template import (
ResourceTemplate,
match_uri_template,
)
from fastmcp.settings import DuplicateBehavior
from fastmcp.utilities.logging import get_logger
logger = get_logger(__name__)
class ResourceManager:
"""Manages FastMCP resources."""
def __init__(
self,
duplicate_behavior: DuplicateBehavior | None = None,
mask_error_details: bool | None = None,
):
"""Initialize the ResourceManager.
Args:
duplicate_behavior: How to handle duplicate resources
(warn, error, replace, ignore)
mask_error_details: Whether to mask error details from exceptions
other than ResourceError
"""
self._resources: dict[str, Resource] = {}
self._templates: dict[str, ResourceTemplate] = {}
self.mask_error_details = mask_error_details or settings.mask_error_details
# Default to "warn" if None is provided
if duplicate_behavior is None:
duplicate_behavior = "warn"
if duplicate_behavior not in DuplicateBehavior.__args__:
raise ValueError(
f"Invalid duplicate_behavior: {duplicate_behavior}. "
f"Must be one of: {', '.join(DuplicateBehavior.__args__)}"
)
self.duplicate_behavior = duplicate_behavior
def add_resource_or_template_from_fn(
self,
fn: Callable[..., Any],
uri: str,
name: str | None = None,
description: str | None = None,
mime_type: str | None = None,
tags: set[str] | None = None,
) -> Resource | ResourceTemplate:
"""Add a resource or template to the manager from a function.
Args:
fn: The function to register as a resource or template
uri: The URI for the resource or template
name: Optional name for the resource or template
description: Optional description of the resource or template
mime_type: Optional MIME type for the resource or template
tags: Optional set of tags for categorizing the resource or template
Returns:
The added resource or template. If a resource or template with the same URI already exists,
returns the existing resource or template.
"""
from fastmcp.server.context import Context
# Check if this should be a template
has_uri_params = "{" in uri and "}" in uri
# check if the function has any parameters (other than injected context)
has_func_params = any(
p
for p in inspect.signature(fn).parameters.values()
if p.annotation is not Context
)
if has_uri_params or has_func_params:
return self.add_template_from_fn(
fn, uri, name, description, mime_type, tags
)
elif not has_uri_params and not has_func_params:
return self.add_resource_from_fn(
fn, uri, name, description, mime_type, tags
)
else:
raise ValueError(
"Invalid resource or template definition due to a "
"mismatch between URI parameters and function parameters."
)
def add_resource_from_fn(
self,
fn: Callable[..., Any],
uri: str,
name: str | None = None,
description: str | None = None,
mime_type: str | None = None,
tags: set[str] | None = None,
) -> Resource:
"""Add a resource to the manager from a function.
Args:
fn: The function to register as a resource
uri: The URI for the resource
name: Optional name for the resource
description: Optional description of the resource
mime_type: Optional MIME type for the resource
tags: Optional set of tags for categorizing the resource
Returns:
The added resource. If a resource with the same URI already exists,
returns the existing resource.
"""
# deprecated in 2.7.0
if settings.deprecation_warnings:
warnings.warn(
"add_resource_from_fn is deprecated. Use Resource.from_function() and call add_resource() instead.",
DeprecationWarning,
stacklevel=2,
)
resource = Resource.from_function(
fn=fn,
uri=uri,
name=name,
description=description,
mime_type=mime_type,
tags=tags,
)
return self.add_resource(resource)
def add_resource(self, resource: Resource, key: str | None = None) -> Resource:
"""Add a resource to the manager.
Args:
resource: A Resource instance to add
key: Optional URI to use as the storage key (if different from resource.uri)
"""
storage_key = key or str(resource.uri)
logger.debug(
"Adding resource",
extra={
"uri": resource.uri,
"storage_key": storage_key,
"type": type(resource).__name__,
"resource_name": resource.name,
},
)
existing = self._resources.get(storage_key)
if existing:
if self.duplicate_behavior == "warn":
logger.warning(f"Resource already exists: {storage_key}")
self._resources[storage_key] = resource
elif self.duplicate_behavior == "replace":
self._resources[storage_key] = resource
elif self.duplicate_behavior == "error":
raise ValueError(f"Resource already exists: {storage_key}")
elif self.duplicate_behavior == "ignore":
return existing
self._resources[storage_key] = resource
return resource
def add_template_from_fn(
self,
fn: Callable[..., Any],
uri_template: str,
name: str | None = None,
description: str | None = None,
mime_type: str | None = None,
tags: set[str] | None = None,
) -> ResourceTemplate:
"""Create a template from a function."""
# deprecated in 2.7.0
if settings.deprecation_warnings:
warnings.warn(
"add_template_from_fn is deprecated. Use ResourceTemplate.from_function() and call add_template() instead.",
DeprecationWarning,
stacklevel=2,
)
template = ResourceTemplate.from_function(
fn,
uri_template=uri_template,
name=name,
description=description,
mime_type=mime_type,
tags=tags,
)
return self.add_template(template)
def add_template(
self, template: ResourceTemplate, key: str | None = None
) -> ResourceTemplate:
"""Add a template to the manager.
Args:
template: A ResourceTemplate instance to add
key: Optional URI template to use as the storage key (if different from template.uri_template)
Returns:
The added template. If a template with the same URI already exists,
returns the existing template.
"""
uri_template_str = str(template.uri_template)
storage_key = key or uri_template_str
logger.debug(
"Adding template",
extra={
"uri_template": uri_template_str,
"storage_key": storage_key,
"type": type(template).__name__,
"template_name": template.name,
},
)
existing = self._templates.get(storage_key)
if existing:
if self.duplicate_behavior == "warn":
logger.warning(f"Template already exists: {storage_key}")
self._templates[storage_key] = template
elif self.duplicate_behavior == "replace":
self._templates[storage_key] = template
elif self.duplicate_behavior == "error":
raise ValueError(f"Template already exists: {storage_key}")
elif self.duplicate_behavior == "ignore":
return existing
self._templates[storage_key] = template
return template
def has_resource(self, uri: AnyUrl | str) -> bool:
"""Check if a resource exists."""
uri_str = str(uri)
if uri_str in self._resources:
return True
for template_key in self._templates.keys():
if match_uri_template(uri_str, template_key):
return True
return False
async def get_resource(self, uri: AnyUrl | str) -> Resource:
"""Get resource by URI, checking concrete resources first, then templates.
Args:
uri: The URI of the resource to get
Raises:
NotFoundError: If no resource or template matching the URI is found.
"""
uri_str = str(uri)
logger.debug("Getting resource", extra={"uri": uri_str})
# First check concrete resources
if resource := self._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():
# Try to match against the storage key (which might be a custom key)
if params := match_uri_template(uri_str, storage_key):
try:
return await template.create_resource(
uri_str,
params=params,
)
# Pass through ResourceErrors as-is
except ResourceError as e:
logger.error(f"Error creating resource from template: {e}")
raise e
# Handle other exceptions
except Exception as e:
logger.error(f"Error creating resource from template: {e}")
if self.mask_error_details:
# Mask internal details
raise ValueError("Error creating resource from template") from e
else:
# Include original error details
raise ValueError(
f"Error creating resource from template: {e}"
) from e
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)
try:
return await resource.read()
# raise ResourceErrors as-is
except ResourceError as e:
logger.error(f"Error reading resource {uri!r}: {e}")
raise e
# 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
def get_resources(self) -> dict[str, Resource]:
"""Get all registered resources, keyed by URI."""
return self._resources
def get_templates(self) -> dict[str, ResourceTemplate]:
"""Get all registered templates, keyed by URI template."""
return self._templates