Rename FastMCP to MCPServer (#1951)

This commit is contained in:
Marcelo Trylesinski 2026-01-25 14:45:52 +01:00 • committed by GitHub
commit 65c614e48e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
31 changed files with 713 additions and 693 deletions

View file

@ -1,4 +1,4 @@
"""FastMCP CLI package."""
"""MCP CLI package."""
from .cli import app

View file

@ -7,7 +7,7 @@ import sys
from pathlib import Path
from typing import Any
from mcp.server.fastmcp.utilities.logging import get_logger
from mcp.server.mcpserver.utilities.logging import get_logger
logger = get_logger(__name__)
@ -49,7 +49,7 @@ def update_claude_config(
with_packages: list[str] | None = None,
env_vars: dict[str, str] | None = None,
) -> bool:
"""Add or update a FastMCP server in Claude's configuration.
"""Add or update an MCP server in Claude's configuration.
Args:
file_spec: Path to the server file, optionally with :object suffix
@ -121,7 +121,7 @@ def update_claude_config(
else: # pragma: no cover
file_spec = str(Path(file_spec).resolve())
# Add fastmcp run command
# Add mcp run command
args.extend(["mcp", "run", file_spec])
server_config: dict[str, Any] = {"command": uv_path, "args": args}

View file

@ -8,7 +8,7 @@ import sys
from pathlib import Path
from typing import Annotated, Any
from mcp.server import FastMCP
from mcp.server import MCPServer
from mcp.server import Server as LowLevelServer
try:
@ -19,9 +19,9 @@ except ImportError: # pragma: no cover
try:
from mcp.cli import claude
from mcp.server.fastmcp.utilities.logging import get_logger
from mcp.server.mcpserver.utilities.logging import get_logger
except ImportError: # pragma: no cover
print("Error: mcp.server.fastmcp is not installed or not in PYTHONPATH")
print("Error: mcp.server is not installed or not in PYTHONPATH")
sys.exit(1)
try:
@ -149,12 +149,10 @@ def _import_server(file: Path, server_object: str | None = None): # pragma: no
Returns:
True if it's supported.
"""
if not isinstance(server_object, FastMCP):
logger.error(f"The server object {object_name} is of type {type(server_object)} (expecting {FastMCP}).")
if not isinstance(server_object, MCPServer):
logger.error(f"The server object {object_name} is of type {type(server_object)} (expecting {MCPServer}).")
if isinstance(server_object, LowLevelServer):
logger.warning(
"Note that only FastMCP server is supported. Low level Server class is not yet supported."
)
logger.warning("Note that only MCPServer is supported. Low level Server class is not yet supported.")
return False
return True
@ -172,8 +170,8 @@ def _import_server(file: Path, server_object: str | None = None): # pragma: no
f"No server object found in {file}. Please either:\n"
"1. Use a standard variable name (mcp, server, or app)\n"
"2. Specify the object name with file:object syntax"
"3. If the server creates the FastMCP object within main() "
" or another function, refactor the FastMCP object to be a "
"3. If the server creates the MCPServer object within main() "
" or another function, refactor the MCPServer object to be a "
" global variable named mcp, server, or app.",
extra={"file": str(file)},
)

View file

@ -10,7 +10,7 @@ import anyio
from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream
from mcp.server import Server
from mcp.server.fastmcp import FastMCP
from mcp.server.mcpserver import MCPServer
from mcp.shared.memory import create_client_server_memory_streams
from mcp.shared.message import SessionMessage
@ -23,7 +23,7 @@ class InMemoryTransport:
stopped when the context manager exits.
Example:
server = FastMCP("test")
server = MCPServer("test")
transport = InMemoryTransport(server)
async with transport.connect() as (read_stream, write_stream):
@ -36,11 +36,11 @@ class InMemoryTransport:
result = await client.call_tool("my_tool", {...})
"""
def __init__(self, server: Server[Any] | FastMCP, *, raise_exceptions: bool = False) -> None:
def __init__(self, server: Server[Any] | MCPServer, *, raise_exceptions: bool = False) -> None:
"""Initialize the in-memory transport.
Args:
server: The MCP server to connect to (Server or FastMCP instance)
server: The MCP server to connect to (Server or MCPServer instance)
raise_exceptions: Whether to raise exceptions from the server
"""
self._server = server
@ -61,10 +61,10 @@ class InMemoryTransport:
Yields:
A tuple of (read_stream, write_stream) for bidirectional communication
"""
# Unwrap FastMCP to get underlying Server
# Unwrap MCPServer to get underlying Server
actual_server: Server[Any]
if isinstance(self._server, FastMCP):
actual_server = self._server._mcp_server # type: ignore[reportPrivateUsage]
if isinstance(self._server, MCPServer):
actual_server = self._server._lowlevel_server # type: ignore[reportPrivateUsage]
else:
actual_server = self._server

View file

@ -8,7 +8,7 @@ from typing import Any
from mcp.client._memory import InMemoryTransport
from mcp.client.session import ClientSession, ElicitationFnT, ListRootsFnT, LoggingFnT, MessageHandlerFnT, SamplingFnT
from mcp.server import Server
from mcp.server.fastmcp import FastMCP
from mcp.server.mcpserver import MCPServer
from mcp.shared.session import ProgressFnT
from mcp.types import (
CallToolResult,
@ -34,14 +34,14 @@ class Client:
"""A high-level MCP client for connecting to MCP servers.
Currently supports in-memory transport for testing. Pass a Server or
FastMCP instance directly to the constructor.
MCPServer instance directly to the constructor.
Example:
```python
from mcp.client import Client
from mcp.server.fastmcp import FastMCP
from mcp.server.mcpserver import MCPServer
server = FastMCP("test")
server = MCPServer("test")
@server.tool()
def add(a: int, b: int) -> int:
@ -55,7 +55,7 @@ class Client:
```
"""
# TODO(felixweinberger): Expand to support all transport types (like FastMCP 2):
# TODO(felixweinberger): Expand to support all transport types:
# - Add ClientTransport base class with connect_session() method
# - Add StreamableHttpTransport, SSETransport, StdioTransport
# - Add infer_transport() to auto-detect transport from input type
@ -64,7 +64,7 @@ class Client:
def __init__(
self,
server: Server[Any] | FastMCP,
server: Server[Any] | MCPServer,
*,
# TODO(Marcelo): When do `raise_exceptions=True` actually raises?
raise_exceptions: bool = False,
@ -79,7 +79,7 @@ class Client:
"""Initialize the client with a server.
Args:
server: The MCP server to connect to (Server or FastMCP instance)
server: The MCP server to connect to (Server or MCPServer instance)
raise_exceptions: Whether to raise exceptions from the server
read_timeout_seconds: Timeout for read operations
sampling_callback: Callback for handling sampling requests

View file

@ -15,7 +15,7 @@ from typing import Any, TypeAlias
import anyio
import httpx
from pydantic import BaseModel
from pydantic import BaseModel, Field
from typing_extensions import Self
import mcp
@ -103,9 +103,9 @@ class ClientSessionGroup:
class _ComponentNames(BaseModel):
"""Used for reverse index to find components."""
prompts: set[str] = set()
resources: set[str] = set()
tools: set[str] = set()
prompts: set[str] = Field(default_factory=set)
resources: set[str] = Field(default_factory=set)
tools: set[str] = Field(default_factory=set)
# Standard MCP components.
_prompts: dict[str, types.Prompt]

View file

@ -1,5 +1,5 @@
from .fastmcp import FastMCP
from .lowlevel import NotificationOptions, Server
from .mcpserver import MCPServer
from .models import InitializationOptions
__all__ = ["Server", "FastMCP", "NotificationOptions", "InitializationOptions"]
__all__ = ["Server", "MCPServer", "NotificationOptions", "InitializationOptions"]

View file

@ -96,7 +96,7 @@ class TokenVerifier(Protocol):
"""Verify a bearer token and return access info if valid."""
# NOTE: FastMCP doesn't render any of these types in the user response, so it's
# NOTE: MCPServer doesn't render any of these types in the user response, so it's
# OK to add fields to subclasses which should not be exposed externally.
AuthorizationCodeT = TypeVar("AuthorizationCodeT", bound=AuthorizationCode)
RefreshTokenT = TypeVar("RefreshTokenT", bound=RefreshToken)

View file

@ -1,8 +0,0 @@
"""FastMCP - A more ergonomic interface for MCP servers."""
from mcp.types import Icon
from .server import Context, FastMCP
from .utilities.types import Audio, Image
__all__ = ["FastMCP", "Context", "Image", "Audio", "Icon"]

View file

@ -1,21 +0,0 @@
"""Custom exceptions for FastMCP."""
class FastMCPError(Exception):
"""Base error for FastMCP."""
class ValidationError(FastMCPError):
"""Error in validating parameters or return values."""
class ResourceError(FastMCPError):
"""Error in resource operations."""
class ToolError(FastMCPError):
"""Error in tool operations."""
class InvalidSignature(Exception):
"""Invalid signature for use with FastMCP."""

View file

@ -1 +0,0 @@
"""FastMCP utility modules."""

View file

@ -0,0 +1,8 @@
"""MCPServer - A more ergonomic interface for MCP servers."""
from mcp.types import Icon
from .server import Context, MCPServer
from .utilities.types import Audio, Image
__all__ = ["MCPServer", "Context", "Image", "Audio", "Icon"]

View file

@ -0,0 +1,21 @@
"""Custom exceptions for MCPServer."""
class MCPServerError(Exception):
"""Base error for MCPServer."""
class ValidationError(MCPServerError):
"""Error in validating parameters or return values."""
class ResourceError(MCPServerError):
"""Error in resource operations."""
class ToolError(MCPServerError):
"""Error in tool operations."""
class InvalidSignature(Exception):
"""Invalid signature for use with MCPServer."""

View file

@ -0,0 +1,4 @@
from .base import Prompt
from .manager import PromptManager
__all__ = ["Prompt", "PromptManager"]

View file

@ -0,0 +1,183 @@
"""Base classes for MCPServer prompts."""
from __future__ import annotations
import inspect
from collections.abc import Awaitable, Callable, Sequence
from typing import TYPE_CHECKING, Any, Literal
import pydantic_core
from pydantic import BaseModel, Field, TypeAdapter, validate_call
from mcp.server.mcpserver.utilities.context_injection import find_context_parameter, inject_context
from mcp.server.mcpserver.utilities.func_metadata import func_metadata
from mcp.types import ContentBlock, Icon, TextContent
if TYPE_CHECKING:
from mcp.server.mcpserver.server import Context
from mcp.server.session import ServerSessionT
from mcp.shared.context import LifespanContextT, RequestT
class Message(BaseModel):
"""Base class for all prompt messages."""
role: Literal["user", "assistant"]
content: ContentBlock
def __init__(self, content: str | ContentBlock, **kwargs: Any):
if isinstance(content, str):
content = TextContent(type="text", text=content)
super().__init__(content=content, **kwargs)
class UserMessage(Message):
"""A message from the user."""
role: Literal["user", "assistant"] = "user"
def __init__(self, content: str | ContentBlock, **kwargs: Any):
super().__init__(content=content, **kwargs)
class AssistantMessage(Message):
"""A message from the assistant."""
role: Literal["user", "assistant"] = "assistant"
def __init__(self, content: str | ContentBlock, **kwargs: Any):
super().__init__(content=content, **kwargs)
message_validator = TypeAdapter[UserMessage | AssistantMessage](UserMessage | AssistantMessage)
SyncPromptResult = str | Message | dict[str, Any] | Sequence[str | Message | dict[str, Any]]
PromptResult = SyncPromptResult | Awaitable[SyncPromptResult]
class PromptArgument(BaseModel):
"""An argument that can be passed to a prompt."""
name: str = Field(description="Name of the argument")
description: str | None = Field(None, description="Description of what the argument does")
required: bool = Field(default=False, description="Whether the argument is required")
class Prompt(BaseModel):
"""A prompt template that can be rendered with parameters."""
name: str = Field(description="Name of the prompt")
title: str | None = Field(None, description="Human-readable title of the prompt")
description: str | None = Field(None, description="Description of what the prompt does")
arguments: list[PromptArgument] | None = Field(None, description="Arguments that can be passed to the prompt")
fn: Callable[..., PromptResult | Awaitable[PromptResult]] = Field(exclude=True)
icons: list[Icon] | None = Field(default=None, description="Optional list of icons for this prompt")
context_kwarg: str | None = Field(None, description="Name of the kwarg that should receive context", exclude=True)
@classmethod
def from_function(
cls,
fn: Callable[..., PromptResult | Awaitable[PromptResult]],
name: str | None = None,
title: str | None = None,
description: str | None = None,
icons: list[Icon] | None = None,
context_kwarg: str | None = None,
) -> Prompt:
"""Create a Prompt from a function.
The function can return:
- A string (converted to a message)
- A Message object
- A dict (converted to a message)
- A sequence of any of the above
"""
func_name = name or fn.__name__
if func_name == "<lambda>": # pragma: no cover
raise ValueError("You must provide a name for lambda functions")
# Find context parameter if it exists
if context_kwarg is None: # pragma: no branch
context_kwarg = find_context_parameter(fn)
# Get schema from func_metadata, excluding context parameter
func_arg_metadata = func_metadata(
fn,
skip_names=[context_kwarg] if context_kwarg is not None else [],
)
parameters = func_arg_metadata.arg_model.model_json_schema()
# Convert parameters to PromptArguments
arguments: list[PromptArgument] = []
if "properties" in parameters: # pragma: no branch
for param_name, param in parameters["properties"].items():
required = param_name in parameters.get("required", [])
arguments.append(
PromptArgument(
name=param_name,
description=param.get("description"),
required=required,
)
)
# ensure the arguments are properly cast
fn = validate_call(fn)
return cls(
name=func_name,
title=title,
description=description or fn.__doc__ or "",
arguments=arguments,
fn=fn,
icons=icons,
context_kwarg=context_kwarg,
)
async def render(
self,
arguments: dict[str, Any] | None = None,
context: Context[ServerSessionT, LifespanContextT, RequestT] | None = None,
) -> list[Message]:
"""Render the prompt with arguments."""
# Validate required arguments
if self.arguments:
required = {arg.name for arg in self.arguments if arg.required}
provided = set(arguments or {})
missing = required - provided
if missing:
raise ValueError(f"Missing required arguments: {missing}")
try:
# Add context to arguments if needed
call_args = inject_context(self.fn, arguments or {}, context, self.context_kwarg)
# Call function and check if result is a coroutine
result = self.fn(**call_args)
if inspect.iscoroutine(result):
result = await result
# Validate messages
if not isinstance(result, list | tuple):
result = [result]
# Convert result to messages
messages: list[Message] = []
for msg in result: # type: ignore[reportUnknownVariableType]
try:
if isinstance(msg, Message):
messages.append(msg)
elif isinstance(msg, dict):
messages.append(message_validator.validate_python(msg))
elif isinstance(msg, str):
content = TextContent(type="text", text=msg)
messages.append(UserMessage(content=content))
else: # pragma: no cover
content = pydantic_core.to_json(msg, fallback=str, indent=2).decode()
messages.append(Message(role="user", content=content))
except Exception: # pragma: no cover
raise ValueError(f"Could not convert prompt result to message: {msg}")
return messages
except Exception as e: # pragma: no cover
raise ValueError(f"Error rendering prompt {self.name}: {e}")

View file

@ -0,0 +1,60 @@
"""Prompt management functionality."""
from __future__ import annotations
from typing import TYPE_CHECKING, Any
from mcp.server.mcpserver.prompts.base import Message, Prompt
from mcp.server.mcpserver.utilities.logging import get_logger
if TYPE_CHECKING:
from mcp.server.mcpserver.server import Context
from mcp.server.session import ServerSessionT
from mcp.shared.context import LifespanContextT, RequestT
logger = get_logger(__name__)
class PromptManager:
"""Manages MCPServer prompts."""
def __init__(self, warn_on_duplicate_prompts: bool = True):
self._prompts: dict[str, Prompt] = {}
self.warn_on_duplicate_prompts = warn_on_duplicate_prompts
def get_prompt(self, name: str) -> Prompt | None:
"""Get prompt by name."""
return self._prompts.get(name)
def list_prompts(self) -> list[Prompt]:
"""List all registered prompts."""
return list(self._prompts.values())
def add_prompt(
self,
prompt: Prompt,
) -> Prompt:
"""Add a prompt to the manager."""
# Check for duplicates
existing = self._prompts.get(prompt.name)
if existing:
if self.warn_on_duplicate_prompts:
logger.warning(f"Prompt already exists: {prompt.name}")
return existing
self._prompts[prompt.name] = prompt
return prompt
async def render_prompt(
self,
name: str,
arguments: dict[str, Any] | None = None,
context: Context[ServerSessionT, LifespanContextT, RequestT] | None = None,
) -> list[Message]:
"""Render a prompt by name with arguments."""
prompt = self.get_prompt(name)
if not prompt:
raise ValueError(f"Unknown prompt: {name}")
return await prompt.render(arguments, context=context)

View file

@ -0,0 +1,23 @@
from .base import Resource
from .resource_manager import ResourceManager
from .templates import ResourceTemplate
from .types import (
BinaryResource,
DirectoryResource,
FileResource,
FunctionResource,
HttpResource,
TextResource,
)
__all__ = [
"Resource",
"TextResource",
"BinaryResource",
"FunctionResource",
"FileResource",
"HttpResource",
"DirectoryResource",
"ResourceTemplate",
"ResourceManager",
]

View file

@ -0,0 +1,48 @@
"""Base classes and interfaces for MCPServer resources."""
import abc
from typing import Any
from pydantic import (
BaseModel,
ConfigDict,
Field,
ValidationInfo,
field_validator,
)
from mcp.types import Annotations, Icon
class Resource(BaseModel, abc.ABC):
"""Base class for all resources."""
model_config = ConfigDict(validate_default=True)
uri: str = Field(default=..., description="URI of the resource")
name: str | None = Field(description="Name of the resource", default=None)
title: str | None = Field(description="Human-readable title of the resource", default=None)
description: str | None = Field(description="Description of the resource", default=None)
mime_type: str = Field(
default="text/plain",
description="MIME type of the resource content",
pattern=r"^[a-zA-Z0-9]+/[a-zA-Z0-9\-+.]+(;\s*[a-zA-Z0-9\-_.]+=[a-zA-Z0-9\-_.]+)*$",
)
icons: list[Icon] | None = Field(default=None, description="Optional list of icons for this resource")
annotations: Annotations | None = Field(default=None, description="Optional annotations for the resource")
meta: dict[str, Any] | None = Field(default=None, description="Optional metadata for this resource")
@field_validator("name", mode="before")
@classmethod
def set_default_name(cls, name: str | None, info: ValidationInfo) -> str:
"""Set default name from URI if not provided."""
if name:
return name
if uri := info.data.get("uri"):
return str(uri)
raise ValueError("Either name or uri must be provided")
@abc.abstractmethod
async def read(self) -> str | bytes:
"""Read the resource content."""
pass # pragma: no cover

View file

@ -0,0 +1,115 @@
"""Resource manager functionality."""
from __future__ import annotations
from collections.abc import Callable
from typing import TYPE_CHECKING, Any
from pydantic import AnyUrl
from mcp.server.mcpserver.resources.base import Resource
from mcp.server.mcpserver.resources.templates import ResourceTemplate
from mcp.server.mcpserver.utilities.logging import get_logger
from mcp.types import Annotations, Icon
if TYPE_CHECKING:
from mcp.server.mcpserver.server import Context
from mcp.server.session import ServerSessionT
from mcp.shared.context import LifespanContextT, RequestT
logger = get_logger(__name__)
class ResourceManager:
"""Manages MCPServer resources."""
def __init__(self, warn_on_duplicate_resources: bool = True):
self._resources: dict[str, Resource] = {}
self._templates: dict[str, ResourceTemplate] = {}
self.warn_on_duplicate_resources = warn_on_duplicate_resources
def add_resource(self, resource: Resource) -> Resource:
"""Add a resource to the manager.
Args:
resource: A Resource instance to add
Returns:
The added resource. If a resource with the same URI already exists,
returns the existing resource.
"""
logger.debug(
"Adding resource",
extra={
"uri": resource.uri,
"type": type(resource).__name__,
"resource_name": resource.name,
},
)
existing = self._resources.get(str(resource.uri))
if existing:
if self.warn_on_duplicate_resources:
logger.warning(f"Resource already exists: {resource.uri}")
return existing
self._resources[str(resource.uri)] = resource
return resource
def add_template(
self,
fn: Callable[..., Any],
uri_template: str,
name: str | None = None,
title: str | None = None,
description: str | None = None,
mime_type: str | None = None,
icons: list[Icon] | None = None,
annotations: Annotations | None = None,
meta: dict[str, Any] | None = None,
) -> ResourceTemplate:
"""Add a template from a function."""
template = ResourceTemplate.from_function(
fn,
uri_template=uri_template,
name=name,
title=title,
description=description,
mime_type=mime_type,
icons=icons,
annotations=annotations,
meta=meta,
)
self._templates[template.uri_template] = template
return template
async def get_resource(
self,
uri: AnyUrl | str,
context: Context[ServerSessionT, LifespanContextT, RequestT] | None = None,
) -> Resource | None:
"""Get resource by URI, checking concrete resources first, then templates."""
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
for template in self._templates.values():
if params := template.matches(uri_str):
try:
return await template.create_resource(uri_str, params, context=context)
except Exception as e: # pragma: no cover
raise ValueError(f"Error creating resource from template: {e}")
raise ValueError(f"Unknown resource: {uri}")
def list_resources(self) -> list[Resource]:
"""List all registered resources."""
logger.debug("Listing resources", extra={"count": len(self._resources)})
return list(self._resources.values())
def list_templates(self) -> list[ResourceTemplate]:
"""List all registered templates."""
logger.debug("Listing templates", extra={"count": len(self._templates)})
return list(self._templates.values())

View file

@ -0,0 +1,127 @@
"""Resource template functionality."""
from __future__ import annotations
import inspect
import re
from collections.abc import Callable
from typing import TYPE_CHECKING, Any
from urllib.parse import unquote
from pydantic import BaseModel, Field, validate_call
from mcp.server.mcpserver.resources.types import FunctionResource, Resource
from mcp.server.mcpserver.utilities.context_injection import find_context_parameter, inject_context
from mcp.server.mcpserver.utilities.func_metadata import func_metadata
from mcp.types import Annotations, Icon
if TYPE_CHECKING:
from mcp.server.mcpserver.server import Context
from mcp.server.session import ServerSessionT
from mcp.shared.context import LifespanContextT, RequestT
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")
icons: list[Icon] | None = Field(default=None, description="Optional list of icons for the resource template")
annotations: Annotations | None = Field(default=None, description="Optional annotations for the resource template")
meta: dict[str, Any] | None = Field(default=None, description="Optional metadata for this resource template")
fn: Callable[..., Any] = Field(exclude=True)
parameters: dict[str, Any] = Field(description="JSON schema for function parameters")
context_kwarg: str | None = Field(None, description="Name of the kwarg that should receive context")
@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,
icons: list[Icon] | None = None,
annotations: Annotations | None = None,
meta: dict[str, Any] | None = None,
context_kwarg: str | None = None,
) -> ResourceTemplate:
"""Create a template from a function."""
func_name = name or fn.__name__
if func_name == "<lambda>":
raise ValueError("You must provide a name for lambda functions") # pragma: no cover
# Find context parameter if it exists
if context_kwarg is None: # pragma: no branch
context_kwarg = find_context_parameter(fn)
# Get schema from func_metadata, excluding context parameter
func_arg_metadata = func_metadata(
fn,
skip_names=[context_kwarg] if context_kwarg is not None else [],
)
parameters = func_arg_metadata.arg_model.model_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",
icons=icons,
annotations=annotations,
meta=meta,
fn=fn,
parameters=parameters,
context_kwarg=context_kwarg,
)
def matches(self, uri: str) -> dict[str, Any] | None:
"""Check if URI matches template and extract parameters.
Extracted parameters are URL-decoded to handle percent-encoded characters.
"""
# Convert template to regex pattern
pattern = self.uri_template.replace("{", "(?P<").replace("}", ">[^/]+)")
match = re.match(f"^{pattern}$", uri)
if match:
# URL-decode all extracted parameter values
return {key: unquote(value) for key, value in match.groupdict().items()}
return None
async def create_resource(
self,
uri: str,
params: dict[str, Any],
context: Context[ServerSessionT, LifespanContextT, RequestT] | None = None,
) -> Resource:
"""Create a resource from the template with the given parameters."""
try:
# Add context to params if needed
params = inject_context(self.fn, params, context, self.context_kwarg)
# 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,
icons=self.icons,
annotations=self.annotations,
meta=self.meta,
fn=lambda: result, # Capture result in closure
)
except Exception as e:
raise ValueError(f"Error creating resource from template: {e}")

Some files were not shown because too many files have changed in this diff Show more