Skip to main content
Glama
resource_manager.py12 kB
"""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

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Lillard01/chatExcel-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server