Upgrade ty to 0.0.39 (#4225)

This commit is contained in:
Jeremiah Lowin 2026-05-30 11:25:05 -04:00 committed by GitHub
commit 8e66b0a47a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
32 changed files with 243 additions and 119 deletions

View file

@ -27,22 +27,26 @@ class ResourceCSP(BaseModel):
connect_domains: list[str] | None = Field(
default=None,
alias="connectDomains",
validation_alias="connectDomains",
serialization_alias="connectDomains",
description="Origins allowed for fetch/XHR/WebSocket (connect-src)",
)
resource_domains: list[str] | None = Field(
default=None,
alias="resourceDomains",
validation_alias="resourceDomains",
serialization_alias="resourceDomains",
description="Origins allowed for scripts, images, styles, fonts (script-src etc.)",
)
frame_domains: list[str] | None = Field(
default=None,
alias="frameDomains",
validation_alias="frameDomains",
serialization_alias="frameDomains",
description="Origins allowed for nested iframes (frame-src)",
)
base_uri_domains: list[str] | None = Field(
default=None,
alias="baseUriDomains",
validation_alias="baseUriDomains",
serialization_alias="baseUriDomains",
description="Allowed base URIs for the document (base-uri)",
)
@ -69,7 +73,8 @@ class ResourcePermissions(BaseModel):
)
clipboard_write: dict[str, Any] | None = Field(
default=None,
alias="clipboardWrite",
validation_alias="clipboardWrite",
serialization_alias="clipboardWrite",
description="Request clipboard-write access",
)
@ -91,7 +96,8 @@ class AppConfig(BaseModel):
resource_uri: str | None = Field(
default=None,
alias="resourceUri",
validation_alias="resourceUri",
serialization_alias="resourceUri",
description="URI of the UI resource (typically ui:// scheme). Tools only.",
)
visibility: list[Literal["app", "model"]] | None = Field(
@ -107,7 +113,8 @@ class AppConfig(BaseModel):
domain: str | None = Field(default=None, description="Domain for the iframe")
prefers_border: bool | None = Field(
default=None,
alias="prefersBorder",
validation_alias="prefersBorder",
serialization_alias="prefersBorder",
description="Whether the UI prefers a visible border",
)

View file

@ -312,7 +312,7 @@ class FileUpload(FastMCPApp):
H3(provider._title)
with If(STATE.stored.length()):
Badge(
STATE.stored.length(), # ty:ignore[invalid-argument-type]
STATE.stored.length(),
variant="secondary",
)
@ -337,8 +337,8 @@ class FileUpload(FastMCPApp):
Row(gap=2, align="center"),
Column(gap=0),
):
Small(Rx("$item.name")) # ty:ignore[invalid-argument-type]
Muted(Rx("$item.type")) # ty:ignore[invalid-argument-type]
Small(Rx("$item.name"))
Muted(Rx("$item.type"))
Button(
"Upload to Server",
@ -356,7 +356,7 @@ class FileUpload(FastMCPApp):
),
],
on_error=ShowToast(
ERROR, # ty:ignore[invalid-argument-type]
ERROR,
variant="error",
),
),
@ -377,12 +377,12 @@ class FileUpload(FastMCPApp):
),
):
with Column(gap=0):
Small(f.name) # ty:ignore[invalid-argument-type]
Muted(f.uploaded_at) # ty:ignore[invalid-argument-type]
Small(f.name)
Muted(f.uploaded_at)
with Row(gap=2):
Badge(f.type, variant="secondary") # ty:ignore[invalid-argument-type]
Badge(f.type, variant="secondary")
Badge(
f.size_display, # ty:ignore[invalid-argument-type]
f.size_display,
variant="outline",
)

View file

@ -203,7 +203,7 @@ class FormInput(FastMCPApp):
if provider._send_message:
on_success_actions.insert(
0,
SendMessage(RESULT), # ty:ignore[invalid-argument-type]
SendMessage(RESULT),
)
from_model_kwargs: dict[str, Any] = {

View file

@ -150,7 +150,7 @@ class GenerativeUI(Provider):
resource_config = AppConfig(csp=csp)
resource = TextResource(
uri=_gen.RESOURCE_URI, # type: ignore[arg-type] # ty:ignore[invalid-argument-type]
uri=_gen.RESOURCE_URI, # type: ignore[arg-type]
name="Prefab Generative Renderer",
text=get_generative_renderer_html(),
mime_type=UI_MIME_TYPE,

View file

@ -1089,7 +1089,7 @@ def _build_picker_html(tools: list[dict[str, Any]]) -> str:
"__json_args__": Rx("__json_args__"),
}
on_error = ShowToast(Rx("$error"), variant="error") # type: ignore[arg-type] # ty:ignore[invalid-argument-type]
on_error = ShowToast(Rx("$error"), variant="error") # type: ignore[arg-type]
input_mode = f"_mode_{name}"
_desc_max_lines = 10

View file

@ -238,7 +238,7 @@ async def run_command(
await run_v1_server_async(server, host=host, port=port, transport=transport)
return
kwargs = {}
kwargs: dict[str, Any] = {}
if transport is not None:
kwargs["transport"] = transport
# Resolve effective transport for the HTTP kwargs guard — transport

View file

@ -74,7 +74,7 @@ def create_elicitation_callback(
f"{result.content!r}"
)
return MCPElicitResult(
_meta=result.meta, # type: ignore[call-arg] # _meta is Pydantic alias for meta field # ty:ignore[unknown-argument]
_meta=result.meta, # type: ignore[call-arg] # _meta is Pydantic alias for meta field
action=result.action,
content=content,
)

View file

@ -4,7 +4,7 @@ from __future__ import annotations
import uuid
import weakref
from typing import TYPE_CHECKING, Any, Literal, overload
from typing import TYPE_CHECKING, Any, Literal, cast, overload
import mcp.types
import pydantic_core
@ -155,6 +155,7 @@ class ClientPromptsMixin:
# Inject trace context into meta for propagation to server
propagated_meta = inject_trace_context(meta)
request_meta = cast(mcp.types.RequestParams.Meta | None, propagated_meta)
# If meta provided, use send_request for SEP-1686 task support
if propagated_meta:
@ -164,7 +165,7 @@ class ClientPromptsMixin:
name=name,
arguments=serialized_arguments,
task=mcp.types.TaskMetadata(**task_dict) if task_dict else None,
_meta=propagated_meta, # type: ignore[unknown-argument] # pydantic alias # ty:ignore[unknown-argument]
_meta=request_meta, # type: ignore[unknown-argument] # pydantic alias
)
)
result = await self._await_with_session_monitoring(
@ -276,6 +277,7 @@ class ClientPromptsMixin:
# Per SEP-1686 final spec: client sends only ttl, server generates taskId
# Inject trace context into meta for propagation to server
propagated_meta = inject_trace_context(meta)
request_meta = cast(mcp.types.RequestParams.Meta | None, propagated_meta)
# Serialize arguments for MCP protocol
serialized_arguments: dict[str, str] | None = None
@ -294,7 +296,7 @@ class ClientPromptsMixin:
name=name,
arguments=serialized_arguments,
task=mcp.types.TaskMetadata(ttl=ttl),
_meta=propagated_meta, # type: ignore[unknown-argument] # pydantic alias # ty:ignore[unknown-argument]
_meta=request_meta, # type: ignore[unknown-argument] # pydantic alias
)
)

View file

@ -4,7 +4,7 @@ from __future__ import annotations
import uuid
import weakref
from typing import TYPE_CHECKING, Any, Literal, overload
from typing import TYPE_CHECKING, Any, Literal, cast, overload
import mcp.types
from pydantic import AnyUrl, RootModel
@ -218,6 +218,7 @@ class ClientResourcesMixin:
# Inject trace context into meta for propagation to server
propagated_meta = inject_trace_context(meta)
request_meta = cast(mcp.types.RequestParams.Meta | None, propagated_meta)
# If meta provided, use send_request for SEP-1686 task support
if propagated_meta:
@ -226,7 +227,7 @@ class ClientResourcesMixin:
params=mcp.types.ReadResourceRequestParams(
uri=uri,
task=mcp.types.TaskMetadata(**task_dict) if task_dict else None,
_meta=propagated_meta, # type: ignore[unknown-argument] # pydantic alias # ty:ignore[unknown-argument]
_meta=request_meta, # type: ignore[unknown-argument] # pydantic alias
)
)
result = await self._await_with_session_monitoring(
@ -340,6 +341,7 @@ class ClientResourcesMixin:
# Per SEP-1686 final spec: client sends only ttl, server generates taskId
# Inject trace context into meta for propagation to server
propagated_meta = inject_trace_context(meta)
request_meta = cast(mcp.types.RequestParams.Meta | None, propagated_meta)
if isinstance(uri, str):
uri = AnyUrl(uri)
@ -348,7 +350,7 @@ class ClientResourcesMixin:
params=mcp.types.ReadResourceRequestParams(
uri=uri,
task=mcp.types.TaskMetadata(ttl=ttl),
_meta=propagated_meta, # type: ignore[unknown-argument] # pydantic alias # ty:ignore[unknown-argument]
_meta=request_meta, # type: ignore[unknown-argument] # pydantic alias
)
)

View file

@ -4,7 +4,7 @@ from __future__ import annotations
import uuid
import weakref
from typing import TYPE_CHECKING, Any, Literal, overload
from typing import TYPE_CHECKING, Any, Literal, cast, overload
import mcp.types
from opentelemetry.trace import Status, StatusCode
@ -342,6 +342,7 @@ class ClientToolsMixin:
# Per SEP-1686 final spec: client sends only ttl, server generates taskId
# Inject trace context into meta for propagation to server
propagated_meta = inject_trace_context(meta)
request_meta = cast(mcp.types.RequestParams.Meta | None, propagated_meta)
# Build request with task metadata
request = mcp.types.CallToolRequest(
@ -349,7 +350,7 @@ class ClientToolsMixin:
name=name,
arguments=arguments or {},
task=mcp.types.TaskMetadata(ttl=ttl),
_meta=propagated_meta, # type: ignore[unknown-argument] # pydantic alias # ty:ignore[unknown-argument]
_meta=request_meta, # type: ignore[unknown-argument] # pydantic alias
)
)

View file

@ -410,7 +410,7 @@ class ToolTask(Task["CallToolResult"]):
mcp_result = mcp.types.CallToolResult(
content=raw_result.content,
structuredContent=raw_result.structured_content,
_meta=raw_result.meta, # type: ignore[call-arg] # _meta is Pydantic alias for meta field # ty:ignore[unknown-argument]
_meta=raw_result.meta, # type: ignore[call-arg] # _meta is Pydantic alias for meta field
)
result = await self._client._parse_call_tool_result(
self._tool_name,

View file

@ -104,7 +104,7 @@ class StdioTransport(ClientTransport):
cwd=self.cwd,
log_file=self.log_file,
# TODO(ty): remove when ty supports Unpack[TypedDict] inference
session_kwargs=session_kwargs, # type: ignore[arg-type] # ty:ignore[invalid-argument-type]
session_kwargs=session_kwargs, # type: ignore[arg-type]
ready_event=self._ready_event,
stop_event=self._stop_event,
session_future=session_future,

View file

@ -340,7 +340,7 @@ class MCPConfig(BaseModel):
if file_path.exists() and (
content := file_path.read_text(encoding="utf-8").strip()
):
return cls.model_validate_json(content) # ty: ignore[possibly-unresolved-reference]
return cls.model_validate_json(content)
raise ValueError(f"No MCP servers defined in the config: {file_path}")

View file

@ -191,7 +191,7 @@ class PromptResult(pydantic.BaseModel):
return GetPromptResult(
description=self.description,
messages=mcp_messages,
_meta=self.meta, # type: ignore[call-arg] # _meta is Pydantic alias for meta field # ty:ignore[unknown-argument]
_meta=self.meta, # type: ignore[call-arg] # _meta is Pydantic alias for meta field
)
@ -229,7 +229,7 @@ class Prompt(FastMCPComponent):
icons=overrides.get("icons", self.icons),
_meta=overrides.get( # type: ignore[call-arg] # _meta is Pydantic alias for meta field
"_meta", self.get_meta()
), # ty:ignore[unknown-argument]
),
)
@classmethod

View file

@ -8,12 +8,14 @@ import json
import warnings
from collections.abc import Callable
from dataclasses import dataclass, field
from types import MethodType
from typing import (
TYPE_CHECKING,
Any,
Literal,
Protocol,
TypeVar,
cast,
overload,
runtime_checkable,
)
@ -484,8 +486,8 @@ def prompt(
task=task,
auth=auth,
)
target = fn.__func__ if hasattr(fn, "__func__") else fn
target.__fastmcp__ = metadata
target = fn.__func__ if isinstance(fn, staticmethod | MethodType) else fn
cast(Any, target).__fastmcp__ = metadata
return fn
def decorator(fn: F, prompt_name: str | None) -> F:

View file

@ -107,14 +107,14 @@ class ResourceContent(pydantic.BaseModel):
uri=AnyUrl(uri) if isinstance(uri, str) else uri,
text=self.content,
mimeType=self.mime_type or "text/plain",
_meta=self.meta, # type: ignore[call-arg] # _meta is Pydantic alias for meta field # ty:ignore[unknown-argument]
_meta=self.meta, # type: ignore[call-arg] # _meta is Pydantic alias for meta field
)
else:
return mcp.types.BlobResourceContents(
uri=AnyUrl(uri) if isinstance(uri, str) else uri,
blob=base64.b64encode(self.content).decode(),
mimeType=self.mime_type or "application/octet-stream",
_meta=self.meta, # type: ignore[call-arg] # _meta is Pydantic alias for meta field # ty:ignore[unknown-argument]
_meta=self.meta, # type: ignore[call-arg] # _meta is Pydantic alias for meta field
)
@ -211,7 +211,7 @@ class ResourceResult(pydantic.BaseModel):
mcp_contents = [item.to_mcp_resource_contents(uri) for item in self.contents]
return mcp.types.ReadResourceResult(
contents=mcp_contents,
_meta=self.meta, # type: ignore[call-arg] # _meta is Pydantic alias for meta field # ty:ignore[unknown-argument]
_meta=self.meta, # type: ignore[call-arg] # _meta is Pydantic alias for meta field
)
@ -418,7 +418,7 @@ class Resource(FastMCPComponent):
annotations=overrides.get("annotations", self.annotations),
_meta=overrides.get( # type: ignore[call-arg] # _meta is Pydantic alias for meta field
"_meta", self.get_meta()
), # ty:ignore[unknown-argument]
),
)
def __repr__(self) -> str:

View file

@ -7,7 +7,16 @@ import inspect
import warnings
from collections.abc import Callable
from dataclasses import dataclass, field
from typing import TYPE_CHECKING, Any, Literal, Protocol, TypeVar, runtime_checkable
from types import MethodType
from typing import (
TYPE_CHECKING,
Any,
Literal,
Protocol,
TypeVar,
cast,
runtime_checkable,
)
from mcp.types import Annotations, Icon
from pydantic import AnyUrl
@ -326,8 +335,8 @@ def resource(
task=task,
auth=auth,
)
target = fn.__func__ if hasattr(fn, "__func__") else fn
target.__fastmcp__ = metadata
target = fn.__func__ if isinstance(fn, staticmethod | MethodType) else fn
cast(Any, target).__fastmcp__ = metadata
return fn
def decorator(fn: F) -> F:

View file

@ -323,7 +323,7 @@ class ResourceTemplate(FastMCPComponent):
annotations=overrides.get("annotations", self.annotations),
_meta=overrides.get( # type: ignore[call-arg] # _meta is Pydantic alias for meta field
"_meta", self.get_meta()
), # ty:ignore[unknown-argument]
),
)
@classmethod

View file

@ -10,7 +10,7 @@ This implementation is based on:
"""
from collections.abc import Sequence
from typing import Literal
from typing import Any, Literal
import httpx
from key_value.aio.protocols import AsyncKeyValue
@ -151,7 +151,7 @@ class OIDCConfiguration(BaseModel):
strict: The strict flag for the configuration
timeout_seconds: HTTP request timeout in seconds
"""
get_kwargs = {}
get_kwargs: dict[str, Any] = {}
if timeout_seconds is not None:
get_kwargs["timeout"] = timeout_seconds

View file

@ -3,7 +3,7 @@ from __future__ import annotations
import weakref
from collections.abc import Awaitable, Callable
from contextlib import AsyncExitStack
from typing import TYPE_CHECKING, Any
from typing import TYPE_CHECKING, Any, cast
import anyio
import mcp.types
@ -28,6 +28,7 @@ from fastmcp.apps.config import UI_EXTENSION_ID
from fastmcp.utilities.logging import get_logger
if TYPE_CHECKING:
from fastmcp.server.middleware import CallNext
from fastmcp.server.server import FastMCP
logger = get_logger(__name__)
@ -131,7 +132,8 @@ class MiddlewareServerSession(ServerSession):
try:
return await self.fastmcp._run_middleware(
mw_context, call_original_handler
mw_context,
cast("CallNext[Any, Any]", call_original_handler),
)
except McpError as e:
# McpError can be thrown from middleware in `on_initialize`
@ -212,19 +214,22 @@ class LowLevelServer(_Server[LifespanResultT, RequestT]):
experimental_capabilities or {},
)
# Set tasks as a first-class field (not experimental) per SEP-1686
capabilities.tasks = get_task_capabilities()
# Advertise MCP Apps extension support (io.modelcontextprotocol/ui)
# Uses the same extra-field pattern as tasks above ServerCapabilities
# Uses the same extra-field pattern as tasks above - ServerCapabilities
# has extra="allow" so this survives serialization.
# Merge with any existing extensions to avoid clobbering other features.
existing_extensions: dict[str, Any] = (
getattr(capabilities, "extensions", None) or {}
existing_extensions_value = (capabilities.model_extra or {}).get("extensions")
existing_extensions = (
existing_extensions_value
if isinstance(existing_extensions_value, dict)
else {}
)
return capabilities.model_copy(
update={
"tasks": get_task_capabilities(),
"extensions": {**existing_extensions, UI_EXTENSION_ID: {}},
}
)
capabilities.extensions = {**existing_extensions, UI_EXTENSION_ID: {}}
return capabilities
async def run(
self,

View file

@ -1,10 +1,9 @@
from __future__ import annotations
import logging
from collections.abc import Awaitable, Sequence
from collections.abc import Awaitable, Callable, Sequence
from dataclasses import dataclass, field, replace
from datetime import datetime, timezone
from functools import partial
from typing import (
TYPE_CHECKING,
Any,
@ -76,6 +75,16 @@ def make_middleware_wrapper(
return wrapper
def make_handler_wrapper(
handler: Callable[..., Awaitable[Any]],
call_next: CallNext[Any, Any],
) -> CallNext[Any, Any]:
async def wrapper(context: MiddlewareContext[Any]) -> Any:
return await handler(context, call_next=call_next)
return wrapper
class Middleware:
"""Base class for FastMCP middleware with dispatching hooks."""
@ -99,29 +108,32 @@ class Middleware:
match context.method:
case "initialize":
handler = partial(self.on_initialize, call_next=handler)
handler = make_handler_wrapper(self.on_initialize, handler)
case "tools/call":
handler = partial(self.on_call_tool, call_next=handler)
handler = make_handler_wrapper(self.on_call_tool, handler)
case "resources/read":
handler = partial(self.on_read_resource, call_next=handler)
handler = make_handler_wrapper(self.on_read_resource, handler)
case "prompts/get":
handler = partial(self.on_get_prompt, call_next=handler)
handler = make_handler_wrapper(self.on_get_prompt, handler)
case "tools/list":
handler = partial(self.on_list_tools, call_next=handler)
handler = make_handler_wrapper(self.on_list_tools, handler)
case "resources/list":
handler = partial(self.on_list_resources, call_next=handler)
handler = make_handler_wrapper(self.on_list_resources, handler)
case "resources/templates/list":
handler = partial(self.on_list_resource_templates, call_next=handler)
handler = make_handler_wrapper(
self.on_list_resource_templates,
handler,
)
case "prompts/list":
handler = partial(self.on_list_prompts, call_next=handler)
handler = make_handler_wrapper(self.on_list_prompts, handler)
match context.type:
case "request":
handler = partial(self.on_request, call_next=handler)
handler = make_handler_wrapper(self.on_request, handler)
case "notification":
handler = partial(self.on_notification, call_next=handler)
handler = make_handler_wrapper(self.on_notification, handler)
handler = partial(self.on_message, call_next=handler)
handler = make_handler_wrapper(self.on_message, handler)
return handler

View file

@ -31,7 +31,7 @@ from __future__ import annotations
from collections.abc import AsyncIterator, Sequence
from contextlib import asynccontextmanager
from functools import partial
from typing import TYPE_CHECKING, Literal, cast
from typing import TYPE_CHECKING, Any, Literal, cast
from typing_extensions import Self
@ -44,7 +44,13 @@ if TYPE_CHECKING:
from fastmcp.prompts.base import Prompt
from fastmcp.resources.base import Resource
from fastmcp.resources.template import ResourceTemplate
from fastmcp.server.transforms import Transform
from fastmcp.server.transforms import (
GetPromptNext,
GetResourceNext,
GetResourceTemplateNext,
GetToolNext,
Transform,
)
from fastmcp.tools.base import Tool
@ -166,12 +172,15 @@ class Provider:
The tool if found (may be marked disabled), None if not found.
"""
async def base(n: str, version: VersionSpec | None = None) -> Tool | None:
async def base(n: str, *, version: VersionSpec | None = None) -> Tool | None:
return await self._get_tool(n, version)
chain = base
chain: GetToolNext = cast("GetToolNext", base)
for transform in self.transforms:
chain = partial(transform.get_tool, call_next=chain)
chain = cast(
"GetToolNext",
partial(cast(Any, transform.get_tool), call_next=chain),
)
return await chain(name, version=version)
@ -250,12 +259,17 @@ class Provider:
The resource if found (may be marked disabled), None if not found.
"""
async def base(u: str, version: VersionSpec | None = None) -> Resource | None:
async def base(
u: str, *, version: VersionSpec | None = None
) -> Resource | None:
return await self._get_resource(u, version)
chain = base
chain: GetResourceNext = cast("GetResourceNext", base)
for transform in self.transforms:
chain = partial(transform.get_resource, call_next=chain)
chain = cast(
"GetResourceNext",
partial(cast(Any, transform.get_resource), call_next=chain),
)
return await chain(uri, version=version)
@ -286,13 +300,19 @@ class Provider:
"""
async def base(
u: str, version: VersionSpec | None = None
u: str, *, version: VersionSpec | None = None
) -> ResourceTemplate | None:
return await self._get_resource_template(u, version)
chain = base
chain: GetResourceTemplateNext = cast("GetResourceTemplateNext", base)
for transform in self.transforms:
chain = partial(transform.get_resource_template, call_next=chain)
chain = cast(
"GetResourceTemplateNext",
partial(
cast(Any, transform.get_resource_template),
call_next=chain,
),
)
return await chain(uri, version=version)
@ -322,12 +342,15 @@ class Provider:
The prompt if found (may be marked disabled), None if not found.
"""
async def base(n: str, version: VersionSpec | None = None) -> Prompt | None:
async def base(n: str, *, version: VersionSpec | None = None) -> Prompt | None:
return await self._get_prompt(n, version)
chain = base
chain: GetPromptNext = cast("GetPromptNext", base)
for transform in self.transforms:
chain = partial(transform.get_prompt, call_next=chain)
chain = cast(
"GetPromptNext",
partial(cast(Any, transform.get_prompt), call_next=chain),
)
return await chain(name, version=version)

View file

@ -243,7 +243,7 @@ class ProxyResource(Resource):
client_factory: ClientFactoryT,
*,
_cached_content: ResourceResult | None = None,
**kwargs,
**kwargs: Any,
):
super().__init__(**kwargs)
self._client_factory = client_factory
@ -1105,7 +1105,7 @@ class ProxyClient(Client[ClientTransportT]):
kwargs["log_handler"] = default_proxy_log_handler
if "progress_handler" not in kwargs:
kwargs["progress_handler"] = default_proxy_progress_handler
super().__init__(**kwargs | {"transport": transport})
super().__init__(transport=transport, **kwargs)
# Enable forwarding of inbound HTTP headers (e.g. authorization) to
# the upstream server. This is only appropriate for proxy clients,

View file

@ -9,7 +9,6 @@ import secrets
import warnings
from collections.abc import (
AsyncIterator,
Awaitable,
Callable,
Sequence,
)
@ -65,7 +64,7 @@ from fastmcp.resources.template import ResourceTemplate
from fastmcp.server.auth import AuthCheck, AuthContext, AuthProvider, run_auth_checks
from fastmcp.server.lifespan import Lifespan
from fastmcp.server.low_level import LowLevelServer
from fastmcp.server.middleware import Middleware, MiddlewareContext
from fastmcp.server.middleware import CallNext, Middleware, MiddlewareContext
from fastmcp.server.mixins import LifespanMixin, MCPOperationsMixin, TransportMixin
from fastmcp.server.providers import LocalProvider, Provider
from fastmcp.server.providers.aggregate import AggregateProvider
@ -480,12 +479,21 @@ class FastMCP(
async def _run_middleware(
self,
context: MiddlewareContext[Any],
call_next: Callable[[MiddlewareContext[Any]], Awaitable[Any]],
call_next: CallNext[Any, Any],
) -> Any:
"""Builds and executes the middleware chain."""
chain = call_next
for mw in reversed(self.middleware):
chain = partial(mw, call_next=chain)
next_chain: CallNext[Any, Any] = chain
async def wrapped(
ctx: MiddlewareContext[Any],
mw: Middleware = mw,
call_next: CallNext[Any, Any] = next_chain,
) -> Any:
return await mw(ctx, call_next)
chain = cast(CallNext[Any, Any], wrapped)
return await chain(context)
def add_middleware(self, middleware: Middleware) -> None:

View file

@ -301,7 +301,7 @@ async def tasks_result_handler(server: FastMCP, params: dict[str, Any]) -> Any:
"io.modelcontextprotocol/related-task": {
"taskId": client_task_id,
}
}, # ty:ignore[unknown-argument]
},
)
# Parse task key to get component key
@ -359,12 +359,12 @@ async def tasks_result_handler(server: FastMCP, params: dict[str, Any]) -> Any:
mcp_result = mcp.types.CallToolResult(
content=content,
structuredContent=structured_content,
_meta=related_task_meta, # type: ignore[call-arg] # _meta is Pydantic alias for meta field # ty:ignore[unknown-argument]
_meta=related_task_meta, # type: ignore[call-arg] # _meta is Pydantic alias for meta field
)
else:
mcp_result = mcp.types.CallToolResult(
content=mcp_result,
_meta=related_task_meta, # type: ignore[call-arg] # _meta is Pydantic alias for meta field # ty:ignore[unknown-argument]
_meta=related_task_meta, # type: ignore[call-arg] # _meta is Pydantic alias for meta field
)
return mcp_result

View file

@ -143,7 +143,7 @@ class ToolResult(BaseModel):
structuredContent=self.structured_content,
content=self.content,
isError=self.is_error,
_meta=self.meta, # type: ignore[call-arg] # _meta is Pydantic alias for meta field # ty:ignore[unknown-argument]
_meta=self.meta, # type: ignore[call-arg] # _meta is Pydantic alias for meta field
)
if self.structured_content is None:
return self.content
@ -215,7 +215,7 @@ class Tool(FastMCPComponent):
execution=overrides.get("execution", self.execution),
_meta=overrides.get( # type: ignore[call-arg] # _meta is Pydantic alias for meta field
"_meta", self.get_meta()
), # ty:ignore[unknown-argument]
),
)
if (

View file

@ -6,6 +6,7 @@ import inspect
import warnings
from collections.abc import Callable
from dataclasses import dataclass, field
from types import MethodType
from typing import (
TYPE_CHECKING,
Annotated,
@ -13,6 +14,7 @@ from typing import (
Literal,
Protocol,
TypeVar,
cast,
overload,
runtime_checkable,
)
@ -517,8 +519,8 @@ def tool(
auth=auth,
run_in_thread=run_in_thread,
)
target = fn.__func__ if hasattr(fn, "__func__") else fn
target.__fastmcp__ = metadata
target = fn.__func__ if isinstance(fn, staticmethod | MethodType) else fn
cast(Any, target).__fastmcp__ = metadata
return fn
def decorator(fn: F, tool_name: str | None) -> F:

View file

@ -98,7 +98,7 @@ dev = [
"pytest-timeout>=2.4.0",
"pytest-xdist>=3.6.1",
"ruff>=0.12.8",
"ty>=0.0.29",
"ty>=0.0.39",
"prek>=0.2.12",
"loq>=0.1.0a3",
"opentelemetry-exporter-otlp-proto-grpc>=1.39.0",
@ -143,6 +143,9 @@ exclude = ["**/node_modules", "**/__pycache__", ".venv", ".git", "dist"]
[tool.ty.environment]
python-version = "3.10"
[tool.ty.analysis]
replace-imports-with-any = ["prefab_ui.**"]
[tool.ty.rules]
division-by-zero = "warn"
possibly-missing-attribute = "warn"

View file

@ -101,6 +101,23 @@ class TestPromptDecorator:
result = cast(DecoratedPrompt, analyze)("Python")
assert result == "Please analyze: Python"
async def test_staticmethod_metadata_is_available_on_unwrapped_function(self):
"""@prompt should attach metadata where staticmethod access can find it."""
class MyClass:
@prompt(name="custom-static-prompt")
@staticmethod
def my_prompt() -> str:
return "hello"
mcp = FastMCP("Test")
mcp.add_prompt(MyClass.my_prompt)
async with Client(mcp) as client:
prompts = await client.list_prompts()
assert any(p.name == "custom-static-prompt" for p in prompts)
def test_prompt_rejects_classmethod_decorator(self):
"""@prompt should reject classmethod-decorated functions."""
with pytest.raises(TypeError, match="classmethod"):

View file

@ -101,6 +101,23 @@ class TestResourceDecorator:
result = cast(DecoratedResource, get_config)()
assert result == {"setting": "value"}
async def test_staticmethod_metadata_is_available_on_unwrapped_function(self):
"""@resource should attach metadata where staticmethod access can find it."""
class MyClass:
@resource("config://static")
@staticmethod
def get_config() -> str:
return "hello"
mcp = FastMCP("Test")
mcp.add_resource(MyClass.get_config)
async with Client(mcp) as client:
resources = await client.list_resources()
assert any(str(r.uri) == "config://static" for r in resources)
def test_resource_rejects_classmethod_decorator(self):
"""@resource should reject classmethod-decorated functions."""

View file

@ -152,6 +152,19 @@ class TestToolDecorator:
result = cast(DecoratedTool, greet)("World")
assert result == "Hello, World!"
def test_staticmethod_metadata_is_available_on_unwrapped_function(self):
"""@tool should attach metadata where staticmethod access can find it."""
class MyClass:
@tool(name="custom-static-tool")
@staticmethod
def my_method() -> str:
return "hello"
created_tool = FunctionTool.from_function(MyClass.my_method)
assert created_tool.name == "custom-static-tool"
def test_tool_rejects_classmethod_decorator(self):
"""@tool should reject classmethod-decorated functions."""
with pytest.raises(TypeError, match="classmethod"):

41
uv.lock generated
View file

@ -10,7 +10,7 @@ resolution-markers = [
]
[options]
exclude-newer = "2026-05-15T22:43:18.553853Z"
exclude-newer = "2026-05-16T14:51:14.412224Z"
exclude-newer-span = "P1W"
[options.exclude-newer-package]
@ -938,7 +938,7 @@ dev = [
{ name = "pytest-timeout", specifier = ">=2.4.0" },
{ name = "pytest-xdist", specifier = ">=3.6.1" },
{ name = "ruff", specifier = ">=0.12.8" },
{ name = "ty", specifier = ">=0.0.29" },
{ name = "ty", specifier = ">=0.0.39" },
]
[[package]]
@ -3150,26 +3150,27 @@ wheels = [
[[package]]
name = "ty"
version = "0.0.29"
version = "0.0.39"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/47/d5/853561de49fae38c519e905b2d8da9c531219608f1fccc47a0fc2c896980/ty-0.0.29.tar.gz", hash = "sha256:e7936cca2f691eeda631876c92809688dbbab68687c3473f526cd83b6a9228d8", size = 5469221, upload-time = "2026-04-05T15:01:21.328Z" }
sdist = { url = "https://files.pythonhosted.org/packages/d0/8d/7b5c74dc287fbcb37bae9853cec13bf44717c1735298500e4aeba31579a9/ty-0.0.39.tar.gz", hash = "sha256:f750277e76a01ecd86185960eca73823c26a53c51103568d56d4d904575159fd", size = 5702365, upload-time = "2026-05-22T21:09:56.403Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/03/b7/911f9962115acfa24e3b2ec9d4992dd994c38e8769e1b1d7680bb4d28a51/ty-0.0.29-py3-none-linux_armv6l.whl", hash = "sha256:b8a40955f7660d3eaceb0d964affc81b790c0765e7052921a5f861ff8a471c30", size = 10568206, upload-time = "2026-04-05T15:01:19.165Z" },
{ url = "https://files.pythonhosted.org/packages/fe/c3/fcae2167d4c77a97269f92f11d1b43b03617f81de1283d5d05b43432110c/ty-0.0.29-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6b6849adae15b00bbe2d3c5b078967dcb62eba37d38936b8eeb4c81a82d2e3b8", size = 10442530, upload-time = "2026-04-05T15:01:28.471Z" },
{ url = "https://files.pythonhosted.org/packages/97/33/5a6bfa240cfcb9c36046ae2459fa9ea23238d20130d8656ff5ac4d6c012a/ty-0.0.29-py3-none-macosx_11_0_arm64.whl", hash = "sha256:dcdd9b17209788152f7b7ea815eda07989152325052fe690013537cc7904ce49", size = 9915735, upload-time = "2026-04-05T15:01:10.365Z" },
{ url = "https://files.pythonhosted.org/packages/b3/1e/318f45fae232118e81a6306c30f50de42c509c412128d5bd231eab699ffb/ty-0.0.29-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9d8ed4789bae78ffaf94462c0d25589a734cab0366b86f2bbcb1bb90e1a7a169", size = 10419748, upload-time = "2026-04-05T15:01:32.375Z" },
{ url = "https://files.pythonhosted.org/packages/a9/a8/5687872e2ab5a0f7dd4fd8456eac31e9381ad4dc74961f6f29965ad4dd91/ty-0.0.29-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:91ec374b8565e0ad0900011c24641ebbef2da51adbd4fb69ff3280c8a7eceb02", size = 10394738, upload-time = "2026-04-05T15:01:06.473Z" },
{ url = "https://files.pythonhosted.org/packages/de/68/015d118097eeb95e6a44c4abce4c0a28b7b9dfb3085b7f0ee48e4f099633/ty-0.0.29-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:298a8d5faa2502d3810bbbb47a030b9455495b9921594206043c785dd61548cf", size = 10910613, upload-time = "2026-04-05T15:01:17.17Z" },
{ url = "https://files.pythonhosted.org/packages/1c/01/47ce3c6c53e0670eadbe80756b167bf80ed6681d1ba57cfde2e8065a13d1/ty-0.0.29-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3c8fba1a3524c6109d1e020d92301c79d41bf442fa8d335b9fa366239339cb70", size = 11475750, upload-time = "2026-04-05T15:01:30.461Z" },
{ url = "https://files.pythonhosted.org/packages/c4/cf/e361845b1081c9264ad5b7c963231bab03f2666865a9f2a115c4233f2137/ty-0.0.29-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4c48adf88a70d264128c39ee922ed14a947817fced1e93c08c1a89c9244edcde", size = 11190055, upload-time = "2026-04-05T15:01:12.369Z" },
{ url = "https://files.pythonhosted.org/packages/79/12/0fb0857e9a62cb11586e9a712103877bbf717f5fb570d16634408cfdefee/ty-0.0.29-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ce0a7a0e96bc7b42518cd3a1a6a6298ef64ff40ca4614355c1aa807059b5c6f", size = 11020539, upload-time = "2026-04-05T15:01:37.022Z" },
{ url = "https://files.pythonhosted.org/packages/20/36/5a26753802083f80cd125db6c4348ad42b3c982ec36e718e0bf4c18f75e5/ty-0.0.29-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a6ac86a05b4a3731d45365ab97780acc7b8146fa62fccb3cbe94fe6546c67a97", size = 10396399, upload-time = "2026-04-05T15:01:26.167Z" },
{ url = "https://files.pythonhosted.org/packages/00/e6/b4e75b5752239ab3ab400f19faef4dbef81d05aab5d3419fda0c062a3765/ty-0.0.29-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:6bbbf53141af0f3150bf288d716263f1a3550054e4b3551ca866d38192ba9891", size = 10421461, upload-time = "2026-04-05T15:01:08.367Z" },
{ url = "https://files.pythonhosted.org/packages/c0/21/1084b5b609f9abed62070ec0b31c283a403832a6310c8bbc208bd45ee1e6/ty-0.0.29-py3-none-musllinux_1_2_i686.whl", hash = "sha256:1c9e06b770c1d0ff5efc51e34312390db31d53fcf3088163f413030b42b74f84", size = 10599187, upload-time = "2026-04-05T15:01:23.52Z" },
{ url = "https://files.pythonhosted.org/packages/ab/a1/ce19a2ca717bbcc1ee11378aba52ef70b6ce5b87245162a729d9fdc2360f/ty-0.0.29-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:0307fe37e3f000ef1a4ae230bbaf511508a78d24a5e51b40902a21b09d5e6037", size = 11121198, upload-time = "2026-04-05T15:01:15.22Z" },
{ url = "https://files.pythonhosted.org/packages/6b/6b/f1430b279af704321566ce7ec2725d3d8258c2f815ebd93e474c64cd4543/ty-0.0.29-py3-none-win32.whl", hash = "sha256:7a2a898217960a825f8bc0087e1fdbaf379606175e98f9807187221d53a4a8ed", size = 9995331, upload-time = "2026-04-05T15:01:01.32Z" },
{ url = "https://files.pythonhosted.org/packages/d2/ef/3ef01c17785ff9a69378465c7d0faccd48a07b163554db0995e5d65a5a23/ty-0.0.29-py3-none-win_amd64.whl", hash = "sha256:fc1294200226b91615acbf34e0a9ad81caf98c081e9c6a912a31b0a7b603bc3f", size = 11023644, upload-time = "2026-04-05T15:01:04.432Z" },
{ url = "https://files.pythonhosted.org/packages/2c/55/87280a994d6a2d2647c65e12abbc997ed49835794366153c04c4d9304d76/ty-0.0.29-py3-none-win_arm64.whl", hash = "sha256:f9794bbd1bb3ce13f78c191d0c89ae4c63f52c12b6daa0c6fe220b90d019d12c", size = 10428165, upload-time = "2026-04-05T15:01:34.665Z" },
{ url = "https://files.pythonhosted.org/packages/08/17/9b89802c26d12d0f7a27bc25d4066d941d42891e8898f9f26499f0067e32/ty-0.0.39-py3-none-linux_armv6l.whl", hash = "sha256:c1bb7ac70f1f7d70cc6655fd96558039e4562b10f489fa49c7ebfd5fcee73ad1", size = 11360431, upload-time = "2026-05-22T21:09:18.689Z" },
{ url = "https://files.pythonhosted.org/packages/9c/c6/663ded50e823dbf9fb9d002eca46b7cb1fb2c72b744b84f22ce732a0ee0b/ty-0.0.39-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:3435b64c1e59c14c9aa39c20cc018823937cd38d55db853e74d95b8f420569b0", size = 11096281, upload-time = "2026-05-22T21:09:15.383Z" },
{ url = "https://files.pythonhosted.org/packages/8b/ae/5d38ba9a6456ff4c78d212cf464fd8b9a25d8118465197b0b2dc891c0b19/ty-0.0.39-py3-none-macosx_11_0_arm64.whl", hash = "sha256:5f136377ce46c73677701a9e1ad730bf72f699bcec046e422eb79d0886cac3ab", size = 10529674, upload-time = "2026-05-22T21:09:46.471Z" },
{ url = "https://files.pythonhosted.org/packages/be/6f/43638cb8106445d3c8817256a0731cde9dd7b6a53ae2e881294bc1930ca3/ty-0.0.39-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:36b65fb0cc17f03e851d40e210d420be94ab8bc52d041328ad1e45f616036a61", size = 11055561, upload-time = "2026-05-22T21:09:36.981Z" },
{ url = "https://files.pythonhosted.org/packages/91/17/95e62cf4458527ce78dc386eba18f8b10c3fb64cd8c9e7e59b262ff6029d/ty-0.0.39-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4967967bfadf3860ff84c3fccdbaec8edf8aa20d0d727521084733d853de6657", size = 11127185, upload-time = "2026-05-22T21:09:31.395Z" },
{ url = "https://files.pythonhosted.org/packages/4e/c0/93666c213db5c71ab1b1f1a0db5f66bf8c7c0e0b0bf59859f5da8f0b3c36/ty-0.0.39-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9e10ecb1297099ddf9a1f054f8bd921d1863ce85fb819a3c96ed27865a1ba6ed", size = 11608459, upload-time = "2026-05-22T21:09:12.862Z" },
{ url = "https://files.pythonhosted.org/packages/79/85/3b26585afc8b50230d6464bb0642feef4fab3f847e38b1f0ffa971a81446/ty-0.0.39-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9b19cca70e465d71b0510656343883d62372bbe74b7845cae7c0e701d6d5264b", size = 12177101, upload-time = "2026-05-22T21:09:40.519Z" },
{ url = "https://files.pythonhosted.org/packages/49/4a/1039e4f6afc576dc1c3a4d22a6478904a1ad3766597cd0b93c077ab9dfce/ty-0.0.39-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:56c6704b01b9b3d80ff26b2918423b742516d1e469bef830e9254dcedc9185bf", size = 11827815, upload-time = "2026-05-22T21:09:49.89Z" },
{ url = "https://files.pythonhosted.org/packages/e2/c5/4688652870e350a76a8157f7ffb59ad54f37d5d10725aa7076f66ac94ec8/ty-0.0.39-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40b7840ff46764b6a6757f4ade1cd0530fc3e8a0b435ca93e7602360e4cb90b6", size = 11694429, upload-time = "2026-05-22T21:09:21.568Z" },
{ url = "https://files.pythonhosted.org/packages/fc/72/8a1c4e823bb5bdc935a1c8140e100304e36a68a4139592f170aa9736fdb7/ty-0.0.39-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:1c62a3a87ce26b50819f0dbf03bd95f23f19eeb87bbc7aa732ec64277c77f1aa", size = 11869846, upload-time = "2026-05-22T21:09:28.053Z" },
{ url = "https://files.pythonhosted.org/packages/17/9f/cf982457b861ae22d657c5dcdbc631199f7f90264279db1d17230dfbc3ff/ty-0.0.39-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:f8c34bc81a9c3516e49904e9d8330aac385377cca98390193ea02b903a40fcf0", size = 11029763, upload-time = "2026-05-22T21:09:06.791Z" },
{ url = "https://files.pythonhosted.org/packages/46/c9/95b64f6d43ae6e8f0b7e13dacf9c196d35819af22b1924171fba31383156/ty-0.0.39-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:66f5ab11586a64e79cb692ad685ee5469325c31b5f30bd3554f52f36dbe28cc4", size = 11146761, upload-time = "2026-05-22T21:09:10.178Z" },
{ url = "https://files.pythonhosted.org/packages/52/69/0a89cfb06f7632a05bf56c78e0affb4a40f81759e275376cea75c9c5abe9/ty-0.0.39-py3-none-musllinux_1_2_i686.whl", hash = "sha256:e8d89732bcbbcb091f439e556dfc4932f198b118b47d5b85212c60662099670e", size = 11281843, upload-time = "2026-05-22T21:09:34.234Z" },
{ url = "https://files.pythonhosted.org/packages/0e/53/64c4a27067a46643fea2b3fcf21a8a2f838d91a65ffdd14f2e82945b9538/ty-0.0.39-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:eceb6c91dcd05a231119f82abdd9aa337513de23ca6ac990bc44f88791dc1799", size = 11792477, upload-time = "2026-05-22T21:09:24.923Z" },
{ url = "https://files.pythonhosted.org/packages/1a/e8/02f4dd4a12bcdbda0006f9c7ff3b99a4be06bd0d257d3bd4a5b66de074e6/ty-0.0.39-py3-none-win32.whl", hash = "sha256:891c3262314dbc80bf3e872634d23dd216306945daa9a9fcc206ce5ed21ac4c9", size = 10615377, upload-time = "2026-05-22T21:09:43.167Z" },
{ url = "https://files.pythonhosted.org/packages/b5/5a/aaeb22faa8d4dae90a287d4c3636c671edcff3b99be5f4fc8b79ad71eef6/ty-0.0.39-py3-none-win_amd64.whl", hash = "sha256:ba7f2d54452535419e90f6f03ff39282999e87b43c21c00559f6d7ad711a36d5", size = 11710711, upload-time = "2026-05-22T21:09:53.179Z" },
{ url = "https://files.pythonhosted.org/packages/a3/17/ae7339651bfcaa5f54698c8c70eaf5031baa400ecb67baec31d03a56cbd4/ty-0.0.39-py3-none-win_arm64.whl", hash = "sha256:eb4cf0fefbbfedf9a352597bb2431ebdcb7eb3a595c0f825f228e897a0ec285d", size = 11081409, upload-time = "2026-05-22T21:09:03.741Z" },
]
[[package]]