Skip to main content
Glama

propublica-mcp

template.py•10.3 kB
"""Resource template functionality.""" from __future__ import annotations import inspect import re from collections.abc import Callable from typing import Any from urllib.parse import unquote from mcp.types import ResourceTemplate as MCPResourceTemplate from pydantic import ( Field, field_validator, validate_call, ) from fastmcp.resources.resource import Resource from fastmcp.server.dependencies import get_context from fastmcp.utilities.components import FastMCPComponent from fastmcp.utilities.json_schema import compress_schema from fastmcp.utilities.types import ( find_kwarg_by_type, get_cached_typeadapter, ) def build_regex(template: str) -> re.Pattern: parts = re.split(r"(\{[^}]+\})", template) pattern = "" for part in parts: if part.startswith("{") and part.endswith("}"): name = part[1:-1] if name.endswith("*"): name = name[:-1] pattern += f"(?P<{name}>.+)" else: pattern += f"(?P<{name}>[^/]+)" else: pattern += re.escape(part) return re.compile(f"^{pattern}$") def match_uri_template(uri: str, uri_template: str) -> dict[str, str] | None: regex = build_regex(uri_template) match = regex.match(uri) if match: return {k: unquote(v) for k, v in match.groupdict().items()} return None class ResourceTemplate(FastMCPComponent): """A template for dynamically creating resources.""" uri_template: str = Field( description="URI template with parameters (e.g. weather://{city}/current)" ) mime_type: str = Field( default="text/plain", description="MIME type of the resource content" ) parameters: dict[str, Any] = Field( description="JSON schema for function parameters" ) def __repr__(self) -> str: return f"{self.__class__.__name__}(uri_template={self.uri_template!r}, name={self.name!r}, description={self.description!r}, tags={self.tags})" def enable(self) -> None: super().enable() try: context = get_context() context._queue_resource_list_changed() # type: ignore[private-use] except RuntimeError: pass # No context available def disable(self) -> None: super().disable() try: context = get_context() context._queue_resource_list_changed() # type: ignore[private-use] except RuntimeError: pass # No context available @staticmethod def from_function( fn: Callable[..., Any], uri_template: str, name: str | None = None, title: str | None = None, description: str | None = None, mime_type: str | None = None, tags: set[str] | None = None, enabled: bool | None = None, ) -> FunctionResourceTemplate: return FunctionResourceTemplate.from_function( fn=fn, uri_template=uri_template, name=name, title=title, description=description, mime_type=mime_type, tags=tags, enabled=enabled, ) @field_validator("mime_type", mode="before") @classmethod def set_default_mime_type(cls, mime_type: str | None) -> str: """Set default MIME type if not provided.""" if mime_type: return mime_type return "text/plain" def matches(self, uri: str) -> dict[str, Any] | None: """Check if URI matches template and extract parameters.""" return match_uri_template(uri, self.uri_template) async def read(self, arguments: dict[str, Any]) -> str | bytes: """Read the resource content.""" raise NotImplementedError( "Subclasses must implement read() or override create_resource()" ) async def create_resource(self, uri: str, params: dict[str, Any]) -> Resource: """Create a resource from the template with the given parameters.""" async def resource_read_fn() -> str | bytes: # Call function and check if result is a coroutine result = await self.read(arguments=params) return result return Resource.from_function( fn=resource_read_fn, uri=uri, name=self.name, description=self.description, mime_type=self.mime_type, tags=self.tags, enabled=self.enabled, ) def to_mcp_template(self, **overrides: Any) -> MCPResourceTemplate: """Convert the resource template to an MCPResourceTemplate.""" kwargs = { "uriTemplate": self.uri_template, "name": self.name, "description": self.description, "mimeType": self.mime_type, "title": self.title, } return MCPResourceTemplate(**kwargs | overrides) @classmethod def from_mcp_template(cls, mcp_template: MCPResourceTemplate) -> ResourceTemplate: """Creates a FastMCP ResourceTemplate from a raw MCP ResourceTemplate object.""" # Note: This creates a simple ResourceTemplate instance. For function-based templates, # the original function is lost, which is expected for remote templates. return cls( uri_template=mcp_template.uriTemplate, name=mcp_template.name, description=mcp_template.description, mime_type=mcp_template.mimeType or "text/plain", parameters={}, # Remote templates don't have local parameters ) @property def key(self) -> str: """ The key of the component. This is used for internal bookkeeping and may reflect e.g. prefixes or other identifiers. You should not depend on keys having a certain value, as the same tool loaded from different hierarchies of servers may have different keys. """ return self._key or self.uri_template class FunctionResourceTemplate(ResourceTemplate): """A template for dynamically creating resources.""" fn: Callable[..., Any] async def read(self, arguments: dict[str, Any]) -> str | bytes: """Read the resource content.""" from fastmcp.server.context import Context # Add context to parameters if needed kwargs = arguments.copy() context_kwarg = find_kwarg_by_type(self.fn, kwarg_type=Context) if context_kwarg and context_kwarg not in kwargs: kwargs[context_kwarg] = get_context() result = self.fn(**kwargs) if inspect.isawaitable(result): result = await result return result @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, tags: set[str] | None = None, enabled: bool | None = None, ) -> FunctionResourceTemplate: """Create a template from a function.""" from fastmcp.server.context import Context func_name = name or getattr(fn, "__name__", None) or fn.__class__.__name__ if func_name == "<lambda>": raise ValueError("You must provide a name for lambda functions") # Reject functions with *args # (**kwargs is allowed because the URI will define the parameter names) sig = inspect.signature(fn) for param in sig.parameters.values(): if param.kind == inspect.Parameter.VAR_POSITIONAL: raise ValueError( "Functions with *args are not supported as resource templates" ) # Auto-detect context parameter if not provided context_kwarg = find_kwarg_by_type(fn, kwarg_type=Context) # Validate that URI params match function params uri_params = set(re.findall(r"{(\w+)(?:\*)?}", uri_template)) if not uri_params: raise ValueError("URI template must contain at least one parameter") func_params = set(sig.parameters.keys()) if context_kwarg: func_params.discard(context_kwarg) # get the parameters that are required required_params = { p for p in func_params if sig.parameters[p].default is inspect.Parameter.empty and sig.parameters[p].kind != inspect.Parameter.VAR_KEYWORD and p != context_kwarg } # Check if required parameters are a subset of the URI parameters if not required_params.issubset(uri_params): raise ValueError( f"Required function arguments {required_params} must be a subset of the URI parameters {uri_params}" ) # Check if the URI parameters are a subset of the function parameters (skip if **kwargs present) if not any( param.kind == inspect.Parameter.VAR_KEYWORD for param in sig.parameters.values() ): if not uri_params.issubset(func_params): raise ValueError( f"URI parameters {uri_params} must be a subset of the function arguments: {func_params}" ) description = description or inspect.getdoc(fn) # if the fn is a callable class, we need to get the __call__ method from here out if not inspect.isroutine(fn): fn = fn.__call__ # if the fn is a staticmethod, we need to work with the underlying function if isinstance(fn, staticmethod): fn = fn.__func__ type_adapter = get_cached_typeadapter(fn) parameters = type_adapter.json_schema() # compress the schema prune_params = [context_kwarg] if context_kwarg else None parameters = compress_schema(parameters, prune_params=prune_params) # ensure the arguments are properly cast fn = validate_call(fn) return cls( uri_template=uri_template, name=func_name, title=title, description=description, mime_type=mime_type or "text/plain", fn=fn, parameters=parameters, tags=tags or set(), enabled=enabled if enabled is not None else True, )

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/asachs01/propublica-mcp'

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