diff --git a/fastmcp_slim/fastmcp/apps/config.py b/fastmcp_slim/fastmcp/apps/config.py index c55cd0b13..3d89aa382 100644 --- a/fastmcp_slim/fastmcp/apps/config.py +++ b/fastmcp_slim/fastmcp/apps/config.py @@ -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", ) diff --git a/fastmcp_slim/fastmcp/apps/file_upload.py b/fastmcp_slim/fastmcp/apps/file_upload.py index aeb890379..ed302ce9d 100644 --- a/fastmcp_slim/fastmcp/apps/file_upload.py +++ b/fastmcp_slim/fastmcp/apps/file_upload.py @@ -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", ) diff --git a/fastmcp_slim/fastmcp/apps/form.py b/fastmcp_slim/fastmcp/apps/form.py index f9b9b0a79..96ee35adf 100644 --- a/fastmcp_slim/fastmcp/apps/form.py +++ b/fastmcp_slim/fastmcp/apps/form.py @@ -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] = { diff --git a/fastmcp_slim/fastmcp/apps/generative.py b/fastmcp_slim/fastmcp/apps/generative.py index b3cbe3c33..0ad78f12d 100644 --- a/fastmcp_slim/fastmcp/apps/generative.py +++ b/fastmcp_slim/fastmcp/apps/generative.py @@ -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, diff --git a/fastmcp_slim/fastmcp/cli/apps_dev.py b/fastmcp_slim/fastmcp/cli/apps_dev.py index 5a6c3003a..cefdd2296 100644 --- a/fastmcp_slim/fastmcp/cli/apps_dev.py +++ b/fastmcp_slim/fastmcp/cli/apps_dev.py @@ -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 diff --git a/fastmcp_slim/fastmcp/cli/run.py b/fastmcp_slim/fastmcp/cli/run.py index 6c95749ab..ba9a2ee46 100644 --- a/fastmcp_slim/fastmcp/cli/run.py +++ b/fastmcp_slim/fastmcp/cli/run.py @@ -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 diff --git a/fastmcp_slim/fastmcp/client/elicitation.py b/fastmcp_slim/fastmcp/client/elicitation.py index 0bfa9a203..32db2a188 100644 --- a/fastmcp_slim/fastmcp/client/elicitation.py +++ b/fastmcp_slim/fastmcp/client/elicitation.py @@ -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, ) diff --git a/fastmcp_slim/fastmcp/client/mixins/prompts.py b/fastmcp_slim/fastmcp/client/mixins/prompts.py index de503b448..5c607ca9f 100644 --- a/fastmcp_slim/fastmcp/client/mixins/prompts.py +++ b/fastmcp_slim/fastmcp/client/mixins/prompts.py @@ -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 ) ) diff --git a/fastmcp_slim/fastmcp/client/mixins/resources.py b/fastmcp_slim/fastmcp/client/mixins/resources.py index 3cc845d50..acc19593c 100644 --- a/fastmcp_slim/fastmcp/client/mixins/resources.py +++ b/fastmcp_slim/fastmcp/client/mixins/resources.py @@ -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 ) ) diff --git a/fastmcp_slim/fastmcp/client/mixins/tools.py b/fastmcp_slim/fastmcp/client/mixins/tools.py index c66c6e1f7..8d45a0423 100644 --- a/fastmcp_slim/fastmcp/client/mixins/tools.py +++ b/fastmcp_slim/fastmcp/client/mixins/tools.py @@ -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 ) ) diff --git a/fastmcp_slim/fastmcp/client/tasks.py b/fastmcp_slim/fastmcp/client/tasks.py index 89662aeff..9f1c89ccd 100644 --- a/fastmcp_slim/fastmcp/client/tasks.py +++ b/fastmcp_slim/fastmcp/client/tasks.py @@ -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, diff --git a/fastmcp_slim/fastmcp/client/transports/stdio.py b/fastmcp_slim/fastmcp/client/transports/stdio.py index 9b3bd8dff..6fa979ef0 100644 --- a/fastmcp_slim/fastmcp/client/transports/stdio.py +++ b/fastmcp_slim/fastmcp/client/transports/stdio.py @@ -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, diff --git a/fastmcp_slim/fastmcp/mcp_config.py b/fastmcp_slim/fastmcp/mcp_config.py index 3943009c9..f7ef47553 100644 --- a/fastmcp_slim/fastmcp/mcp_config.py +++ b/fastmcp_slim/fastmcp/mcp_config.py @@ -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}") diff --git a/fastmcp_slim/fastmcp/prompts/base.py b/fastmcp_slim/fastmcp/prompts/base.py index 0b8680d47..028819318 100644 --- a/fastmcp_slim/fastmcp/prompts/base.py +++ b/fastmcp_slim/fastmcp/prompts/base.py @@ -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 diff --git a/fastmcp_slim/fastmcp/prompts/function_prompt.py b/fastmcp_slim/fastmcp/prompts/function_prompt.py index ffc5a35be..3667ece78 100644 --- a/fastmcp_slim/fastmcp/prompts/function_prompt.py +++ b/fastmcp_slim/fastmcp/prompts/function_prompt.py @@ -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: diff --git a/fastmcp_slim/fastmcp/resources/base.py b/fastmcp_slim/fastmcp/resources/base.py index c3024eff3..618db0013 100644 --- a/fastmcp_slim/fastmcp/resources/base.py +++ b/fastmcp_slim/fastmcp/resources/base.py @@ -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: diff --git a/fastmcp_slim/fastmcp/resources/function_resource.py b/fastmcp_slim/fastmcp/resources/function_resource.py index 48a1d99c2..64e359434 100644 --- a/fastmcp_slim/fastmcp/resources/function_resource.py +++ b/fastmcp_slim/fastmcp/resources/function_resource.py @@ -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: diff --git a/fastmcp_slim/fastmcp/resources/template.py b/fastmcp_slim/fastmcp/resources/template.py index f3be91a1f..919c43f8c 100644 --- a/fastmcp_slim/fastmcp/resources/template.py +++ b/fastmcp_slim/fastmcp/resources/template.py @@ -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 diff --git a/fastmcp_slim/fastmcp/server/auth/oidc_proxy.py b/fastmcp_slim/fastmcp/server/auth/oidc_proxy.py index c34f0d85a..a70ec2c3e 100644 --- a/fastmcp_slim/fastmcp/server/auth/oidc_proxy.py +++ b/fastmcp_slim/fastmcp/server/auth/oidc_proxy.py @@ -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 diff --git a/fastmcp_slim/fastmcp/server/low_level.py b/fastmcp_slim/fastmcp/server/low_level.py index e9dda4e35..9168cb7b4 100644 --- a/fastmcp_slim/fastmcp/server/low_level.py +++ b/fastmcp_slim/fastmcp/server/low_level.py @@ -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, diff --git a/fastmcp_slim/fastmcp/server/middleware/middleware.py b/fastmcp_slim/fastmcp/server/middleware/middleware.py index ce7f13567..2786d6835 100644 --- a/fastmcp_slim/fastmcp/server/middleware/middleware.py +++ b/fastmcp_slim/fastmcp/server/middleware/middleware.py @@ -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 diff --git a/fastmcp_slim/fastmcp/server/providers/base.py b/fastmcp_slim/fastmcp/server/providers/base.py index 2025c4353..580e2821d 100644 --- a/fastmcp_slim/fastmcp/server/providers/base.py +++ b/fastmcp_slim/fastmcp/server/providers/base.py @@ -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) diff --git a/fastmcp_slim/fastmcp/server/providers/proxy.py b/fastmcp_slim/fastmcp/server/providers/proxy.py index 53fc6d8a3..ce0beb3a9 100644 --- a/fastmcp_slim/fastmcp/server/providers/proxy.py +++ b/fastmcp_slim/fastmcp/server/providers/proxy.py @@ -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, diff --git a/fastmcp_slim/fastmcp/server/server.py b/fastmcp_slim/fastmcp/server/server.py index 190e76d67..6a5fa00f3 100644 --- a/fastmcp_slim/fastmcp/server/server.py +++ b/fastmcp_slim/fastmcp/server/server.py @@ -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: diff --git a/fastmcp_slim/fastmcp/server/tasks/requests.py b/fastmcp_slim/fastmcp/server/tasks/requests.py index 6c062de0c..474f166cc 100644 --- a/fastmcp_slim/fastmcp/server/tasks/requests.py +++ b/fastmcp_slim/fastmcp/server/tasks/requests.py @@ -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 diff --git a/fastmcp_slim/fastmcp/tools/base.py b/fastmcp_slim/fastmcp/tools/base.py index cbfc45dae..90f550555 100644 --- a/fastmcp_slim/fastmcp/tools/base.py +++ b/fastmcp_slim/fastmcp/tools/base.py @@ -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 ( diff --git a/fastmcp_slim/fastmcp/tools/function_tool.py b/fastmcp_slim/fastmcp/tools/function_tool.py index 91b9996e0..18466a0f7 100644 --- a/fastmcp_slim/fastmcp/tools/function_tool.py +++ b/fastmcp_slim/fastmcp/tools/function_tool.py @@ -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: diff --git a/pyproject.toml b/pyproject.toml index 9ac5a6c3d..32738a1e5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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" diff --git a/tests/prompts/test_standalone_decorator.py b/tests/prompts/test_standalone_decorator.py index 14d642bde..ba5bb5ee9 100644 --- a/tests/prompts/test_standalone_decorator.py +++ b/tests/prompts/test_standalone_decorator.py @@ -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"): diff --git a/tests/resources/test_standalone_decorator.py b/tests/resources/test_standalone_decorator.py index f1b1dbd40..c5dd76ec8 100644 --- a/tests/resources/test_standalone_decorator.py +++ b/tests/resources/test_standalone_decorator.py @@ -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.""" diff --git a/tests/tools/test_standalone_decorator.py b/tests/tools/test_standalone_decorator.py index 964fef813..4d57919cd 100644 --- a/tests/tools/test_standalone_decorator.py +++ b/tests/tools/test_standalone_decorator.py @@ -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"): diff --git a/uv.lock b/uv.lock index 4dd950085..58c47086b 100644 --- a/uv.lock +++ b/uv.lock @@ -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]]