feat: RFC 6570 URI templates with operator-aware security (#2356)
This commit is contained in:
parent
067f90578c
commit
24717cc8eb
1 changed files with 3800 additions and 37 deletions
|
|
@ -4,7 +4,6 @@ from __future__ import annotations
|
|||
|
||||
import base64
|
||||
import inspect
|
||||
import re
|
||||
from collections.abc import AsyncIterator, Awaitable, Callable, Iterable
|
||||
from contextlib import AbstractAsyncContextManager, asynccontextmanager
|
||||
from typing import Any, Generic, Literal, TypeVar, overload
|
||||
|
|
@ -62,7 +61,13 @@ from mcp.server.lowlevel.server import lifespan as default_lifespan
|
|||
from mcp.server.mcpserver.context import Context
|
||||
from mcp.server.mcpserver.exceptions import ResourceError, ResourceNotFoundError
|
||||
from mcp.server.mcpserver.prompts import Prompt, PromptManager
|
||||
from mcp.server.mcpserver.resources import FunctionResource, Resource, ResourceManager
|
||||
from mcp.server.mcpserver.resources import (
|
||||
DEFAULT_RESOURCE_SECURITY,
|
||||
FunctionResource,
|
||||
Resource,
|
||||
ResourceManager,
|
||||
ResourceSecurity,
|
||||
)
|
||||
from mcp.server.mcpserver.tools import Tool, ToolManager
|
||||
from mcp.server.mcpserver.utilities.context_injection import find_context_parameter
|
||||
from mcp.server.mcpserver.utilities.logging import configure_logging, get_logger
|
||||
|
|
@ -72,6 +77,7 @@ from mcp.server.streamable_http import EventStore
|
|||
from mcp.server.streamable_http_manager import StreamableHTTPSessionManager
|
||||
from mcp.server.transport_security import TransportSecuritySettings
|
||||
from mcp.shared.exceptions import MCPError
|
||||
from mcp.shared.uri_template import UriTemplate
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
|
@ -150,7 +156,9 @@ class MCPServer(Generic[LifespanResultT]):
|
|||
dependencies: list[str] | None = None,
|
||||
lifespan: Callable[[MCPServer[LifespanResultT]], AbstractAsyncContextManager[LifespanResultT]] | None = None,
|
||||
auth: AuthSettings | None = None,
|
||||
resource_security: ResourceSecurity = DEFAULT_RESOURCE_SECURITY,
|
||||
):
|
||||
self._resource_security = resource_security
|
||||
self.settings = Settings(
|
||||
debug=debug,
|
||||
log_level=log_level,
|
||||
|
|
@ -622,6 +630,7 @@ class MCPServer(Generic[LifespanResultT]):
|
|||
icons: list[Icon] | None = None,
|
||||
annotations: Annotations | None = None,
|
||||
meta: dict[str, Any] | None = None,
|
||||
security: ResourceSecurity | None = None,
|
||||
) -> Callable[[_CallableT], _CallableT]:
|
||||
"""Decorator to register a function as a resource.
|
||||
|
||||
|
|
@ -631,8 +640,9 @@ class MCPServer(Generic[LifespanResultT]):
|
|||
- bytes for binary content
|
||||
- other types will be converted to JSON
|
||||
|
||||
If the URI contains parameters (e.g. "resource://{param}") or the function
|
||||
has parameters, it will be registered as a template resource.
|
||||
If the URI contains parameters (e.g. "resource://{param}"), it is
|
||||
registered as a template resource. Otherwise it is registered as a
|
||||
static resource; function parameters on a static URI raise an error.
|
||||
|
||||
Args:
|
||||
uri: URI for the resource (e.g. "resource://my-resource" or "resource://{param}")
|
||||
|
|
@ -643,6 +653,9 @@ class MCPServer(Generic[LifespanResultT]):
|
|||
icons: Optional list of icons for the resource
|
||||
annotations: Optional annotations for the resource
|
||||
meta: Optional metadata dictionary for the resource
|
||||
security: Path-safety policy for extracted template parameters.
|
||||
Defaults to the server's ``resource_security`` setting.
|
||||
Only applies to template resources.
|
||||
|
||||
Example:
|
||||
```python
|
||||
|
|
@ -664,6 +677,15 @@ class MCPServer(Generic[LifespanResultT]):
|
|||
data = await fetch_weather(city)
|
||||
return f"Weather for {city}: {data}"
|
||||
```
|
||||
|
||||
Raises:
|
||||
InvalidUriTemplate: If ``uri`` is not a valid RFC 6570 template.
|
||||
ValueError: If URI template parameters don't match the
|
||||
function's parameters, or if a parameter bound to a
|
||||
``{?...}``/``{&...}`` query variable has no default
|
||||
(the client may omit it).
|
||||
TypeError: If the decorator is applied without being called
|
||||
(``@resource`` instead of ``@resource("uri")``).
|
||||
"""
|
||||
# Check if user passed function directly instead of calling decorator
|
||||
if callable(uri):
|
||||
|
|
@ -672,27 +694,44 @@ class MCPServer(Generic[LifespanResultT]):
|
|||
"Did you forget to call it? Use @resource('uri') instead of @resource"
|
||||
)
|
||||
|
||||
# Parse once, early — surfaces malformed-template errors at
|
||||
# decoration time with a clear position, and gives us correct
|
||||
# variable names for all RFC 6570 operators.
|
||||
parsed = UriTemplate.parse(uri)
|
||||
uri_params = set(parsed.variable_names)
|
||||
|
||||
def decorator(fn: _CallableT) -> _CallableT:
|
||||
# Check if this should be a template
|
||||
sig = inspect.signature(fn)
|
||||
has_uri_params = "{" in uri and "}" in uri
|
||||
has_func_params = bool(sig.parameters)
|
||||
|
||||
if has_uri_params or has_func_params:
|
||||
# Check for Context parameter to exclude from validation
|
||||
context_param = find_context_parameter(fn)
|
||||
|
||||
# Validate that URI params match function params (excluding context)
|
||||
uri_params = set(re.findall(r"{(\w+)}", uri))
|
||||
# We need to remove the context_param from the resource function if
|
||||
# there is any.
|
||||
func_params = {p for p in sig.parameters.keys() if p != context_param}
|
||||
context_param = find_context_parameter(fn)
|
||||
func_params = {p for p in sig.parameters.keys() if p != context_param}
|
||||
|
||||
# Template/static is decided purely by the URI: variables
|
||||
# present means template, none means static.
|
||||
if uri_params:
|
||||
if uri_params != func_params:
|
||||
raise ValueError(
|
||||
f"Mismatch between URI parameters {uri_params} and function parameters {func_params}"
|
||||
)
|
||||
|
||||
# A {?...}/{&...} query variable is optional on the wire:
|
||||
# match() omits it from the extracted parameters when the
|
||||
# client leaves it out of the URI. The handler parameter
|
||||
# bound to it must therefore have a Python default; without
|
||||
# one, the author only finds out on the first request that
|
||||
# omits it, as an opaque internal error.
|
||||
missing_defaults = sorted(
|
||||
name
|
||||
for name in parsed.query_variable_names
|
||||
if sig.parameters[name].default is inspect.Parameter.empty
|
||||
)
|
||||
if missing_defaults:
|
||||
raise ValueError(
|
||||
f"Resource {uri!r}: query parameter(s) {missing_defaults} have no "
|
||||
f"default value. A client may omit a {{?...}}/{{&...}} query "
|
||||
f"parameter, so the matching handler parameter must declare a "
|
||||
f"default."
|
||||
)
|
||||
|
||||
# Register as template
|
||||
self._resource_manager.add_template(
|
||||
fn=fn,
|
||||
|
|
@ -703,9 +742,24 @@ class MCPServer(Generic[LifespanResultT]):
|
|||
mime_type=mime_type,
|
||||
icons=icons,
|
||||
annotations=annotations,
|
||||
security=security if security is not None else self._resource_security,
|
||||
meta=meta,
|
||||
)
|
||||
else:
|
||||
if func_params:
|
||||
raise ValueError(
|
||||
f"Resource {uri!r} has no URI template variables, but the "
|
||||
f"handler declares parameters {func_params}. Add matching "
|
||||
f"{{...}} variables to the URI or remove the parameters."
|
||||
)
|
||||
if context_param is not None:
|
||||
raise ValueError(
|
||||
f"Resource {uri!r} has no URI template variables, but the "
|
||||
f"handler declares a Context parameter. Context injection "
|
||||
f"for static resources is not supported. "
|
||||
f"Add a template variable to the URI or remove the "
|
||||
f"Context parameter."
|
||||
)
|
||||
# Register as regular resource
|
||||
resource = FunctionResource.from_function(
|
||||
fn=fn,
|
||||
|
|
|
|||
Loading…
Reference in a new issue