simbiot-harness/venv/lib/python3.12/site-packages/mcp/server/mcpserver/resources/templates.py
Simbiot Harness b60439152b Simbiot Harness v0.1: тактовая лестница на Strands + CQRS/ES-спина
- harness/es.py: append-only event store + проекции (образец — кооп-портал)
- harness/levels.py: уровни 1с/1м/1ч/1д, контекст ∝ масштабу
- harness/scheduler.py: уровневые потоки, идемпотентные тики, absorbed-стабы
- harness/isolated_runner.py + _llm_worker.py: LLM-тик в сабпроцессе
  (глобальный asyncio-рантайм Strands не переживает 2+ create_harness)
- cli.py: init/task/tasks/status/events + requeue (написан самим минутным агентом)
- 9router: отдельный ключ sk-simbiot, модель auto (glm-5.3 первым)
2026-09-29 15:28:50 +00:00

248 lines
9.9 KiB
Python

"""Resource template functionality."""
from __future__ import annotations
import functools
from collections.abc import Callable, Mapping, Set
from dataclasses import dataclass, field
from typing import TYPE_CHECKING, Any
import anyio.to_thread
from mcp_types import Annotations, Icon, InputRequiredResult
from pydantic import BaseModel, Field, validate_call
from mcp.server.mcpserver.exceptions import ResourceError, UnexpectedResourceError
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.shared._callable_inspection import is_async_callable
from mcp.shared.exceptions import MCPError
from mcp.shared.path_security import contains_path_traversal, is_absolute_path
from mcp.shared.uri_template import UriTemplate
if TYPE_CHECKING:
from mcp.server.context import LifespanContextT, RequestT
from mcp.server.mcpserver.context import Context
@dataclass(frozen=True)
class ResourceSecurity:
"""Security policy applied to extracted resource template parameters.
These checks run after :meth:`~mcp.shared.uri_template.UriTemplate.match`
has extracted and decoded parameter values. They catch path-traversal
and absolute-path injection regardless of how the value was encoded in
the URI (literal, ``%2F``, ``%5C``, ``%2E%2E``).
Example::
# Opt out for a parameter that legitimately contains ..
@mcp.resource(
"git://diff/{+range}",
security=ResourceSecurity(exempt_params={"range"}),
)
def git_diff(range: str) -> str: ...
"""
reject_path_traversal: bool = True
"""Reject values containing ``..`` as a path component."""
reject_absolute_paths: bool = True
"""Reject values that look like absolute filesystem paths."""
reject_null_bytes: bool = True
"""Reject values containing NUL (``\\x00``). Null bytes defeat string
comparisons (``"..\\x00" != ".."``) and can cause truncation in C
extensions or subprocess calls."""
exempt_params: Set[str] = field(default_factory=frozenset[str])
"""Parameter names to skip all checks for."""
def validate(self, params: Mapping[str, str | list[str]]) -> str | None:
"""Check all parameter values against the configured policy.
Args:
params: Extracted template parameters. List values (from
explode variables) are checked element-wise.
Returns:
The name of the first parameter that fails, or ``None`` if
all values pass.
"""
for name, value in params.items():
if name in self.exempt_params:
continue
values = value if isinstance(value, list) else [value]
for v in values:
if self.reject_null_bytes and "\0" in v:
return name
if self.reject_path_traversal and contains_path_traversal(v):
return name
if self.reject_absolute_paths and is_absolute_path(v):
return name
return None
DEFAULT_RESOURCE_SECURITY = ResourceSecurity()
"""Secure-by-default policy: traversal, absolute paths, and null bytes rejected."""
class ResourceSecurityError(ValueError):
"""Raised when an extracted parameter fails :class:`ResourceSecurity` checks.
Distinct from a simple ``None`` non-match so that template
iteration can stop at the first security rejection rather than
falling through to a later, possibly more permissive, template.
"""
def __init__(self, template: str, param: str) -> None:
super().__init__(f"Parameter {param!r} of template {template!r} failed security validation")
self.template = template
self.param = param
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")
parsed_template: UriTemplate = Field(exclude=True, description="Parsed RFC 6570 template")
security: ResourceSecurity = Field(exclude=True, description="Path-safety policy for extracted 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,
icons: list[Icon] | None = None,
annotations: Annotations | None = None,
meta: dict[str, Any] | None = None,
context_kwarg: str | None = None,
security: ResourceSecurity = DEFAULT_RESOURCE_SECURITY,
) -> ResourceTemplate:
"""Create a template from a function.
Raises:
InvalidUriTemplate: If ``uri_template`` is malformed or uses
unsupported RFC 6570 features.
"""
func_name = name or fn.__name__
if func_name == "<lambda>":
raise ValueError("You must provide a name for lambda functions") # pragma: no cover
parsed = UriTemplate.parse(uri_template)
# Find context parameter if it exists
if context_kwarg is None: # pragma: no branch
context_kwarg = find_context_parameter(fn)
# Only the argument model is needed; a resource has no output schema to derive
func_arg_metadata = func_metadata(
fn, skip_names=[context_kwarg] if context_kwarg is not None else [], structured_output=False
)
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,
parsed_template=parsed,
security=security,
)
def matches(self, uri: str) -> dict[str, str | list[str]] | None:
"""Check if a URI matches this template and extract parameters.
Delegates to :meth:`UriTemplate.match` for RFC 6570 extraction,
then applies this template's :class:`ResourceSecurity` policy
(path traversal, absolute paths).
Returns:
Extracted parameters on success, or ``None`` if the URI
doesn't match the template.
Raises:
ResourceSecurityError: If the URI matches but an extracted
parameter fails security validation. Raising (rather
than returning ``None``) prevents the resource manager
from silently falling through to a later, possibly more
permissive, template.
"""
params = self.parsed_template.match(uri)
if params is None:
return None
failed = self.security.validate(params)
if failed is not None:
raise ResourceSecurityError(self.uri_template, failed)
return params
async def create_resource(
self,
uri: str,
params: dict[str, Any],
context: Context[LifespanContextT, RequestT],
) -> Resource | InputRequiredResult:
"""Create a resource from the template with the given parameters.
An `InputRequiredResult` returned by the template function is passed
through unchanged (the 2026-07-28 multi-round-trip flow); the retry's
answers arrive on `ctx.input_responses`, with `ctx.request_state`
carrying the echoed opaque state.
Raises:
ResourceError: If the template function raises `ResourceError`.
UnexpectedResourceError: If the template function raises anything other
than `ResourceError` or `MCPError`. `__cause__` is the original exception.
"""
try:
# Add context to params if needed
params = inject_context(self.fn, params, context, self.context_kwarg)
fn = self.fn
if is_async_callable(fn):
result = await fn(**params)
else:
result = await anyio.to_thread.run_sync(functools.partial(self.fn, **params))
if isinstance(result, InputRequiredResult):
return 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 (ResourceError, MCPError):
raise
except Exception as exc:
raise UnexpectedResourceError(f"Error creating resource from template {uri}") from exc