"""Resource template functionality.""" from __future__ import annotations import inspect import re from collections.abc import Callable from typing import Any from pydantic import BaseModel, Field, TypeAdapter, validate_call from mcp.server.fastmcp.resources.types import FunctionResource, Resource class ResourceTemplate(BaseModel): """A template for dynamically creating resources.""" uri_template: str = Field(description="URI template with parameters (e.g. weather://{city}/current)") name: str = Field(description="Name of the resource") title: str | None = Field(description="Human-readable title of the resource", default=None) description: str | None = Field(description="Description of what the resource does") mime_type: str = Field(default="text/plain", description="MIME type of the resource content") fn: Callable[..., Any] = Field(exclude=True) parameters: dict[str, Any] = Field(description="JSON schema for function parameters") @classmethod def from_function( cls, fn: Callable[..., Any], uri_template: str, name: str | None = None, title: str | None = None, description: str | None = None, mime_type: str | None = None, ) -> ResourceTemplate: """Create a template from a function.""" func_name = name or fn.__name__ if func_name == "": raise ValueError("You must provide a name for lambda functions") # Get schema from TypeAdapter - will fail if function isn't properly typed parameters = TypeAdapter(fn).json_schema() # ensure the arguments are properly cast fn = validate_call(fn) return cls( uri_template=uri_template, name=func_name, title=title, description=description or fn.__doc__ or "", mime_type=mime_type or "text/plain", fn=fn, parameters=parameters, ) def matches(self, uri: str) -> dict[str, Any] | None: """Check if URI matches template and extract parameters.""" # Convert template to regex pattern pattern = self.uri_template.replace("{", "(?P<").replace("}", ">[^/]+)") match = re.match(f"^{pattern}$", uri) if match: return match.groupdict() return None async def create_resource(self, uri: str, params: dict[str, Any]) -> Resource: """Create a resource from the template with the given parameters.""" try: # Call function and check if result is a coroutine result = self.fn(**params) if inspect.iscoroutine(result): result = await result return FunctionResource( uri=uri, # type: ignore name=self.name, title=self.title, description=self.description, mime_type=self.mime_type, fn=lambda: result, # Capture result in closure ) except Exception as e: raise ValueError(f"Error creating resource from template: {e}")