From 4177d8358d1950bc517266d5d76173478f9c45c9 Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Mon, 22 Dec 2025 19:47:20 -0500 Subject: [PATCH] Fix ty 0.0.5 type errors (#2676) --- .pre-commit-config.yaml | 2 +- pyproject.toml | 22 ++++++---- src/fastmcp/client/client.py | 2 +- src/fastmcp/client/elicitation.py | 2 +- src/fastmcp/client/tasks.py | 2 +- src/fastmcp/prompts/prompt.py | 4 +- src/fastmcp/resources/resource.py | 6 +-- src/fastmcp/resources/template.py | 2 +- src/fastmcp/server/auth/oauth_proxy.py | 8 ++-- src/fastmcp/server/auth/providers/aws.py | 4 ++ src/fastmcp/server/auth/providers/azure.py | 5 +++ src/fastmcp/server/auth/providers/discord.py | 4 ++ src/fastmcp/server/auth/providers/github.py | 4 ++ src/fastmcp/server/auth/providers/google.py | 4 ++ src/fastmcp/server/auth/providers/workos.py | 4 ++ src/fastmcp/server/context.py | 46 ++++++++++++++++++-- src/fastmcp/server/dependencies.py | 2 +- src/fastmcp/server/middleware/caching.py | 14 +++--- src/fastmcp/server/server.py | 26 ++++++++--- src/fastmcp/server/tasks/handlers.py | 2 +- src/fastmcp/server/tasks/requests.py | 33 ++++++++++---- src/fastmcp/tools/tool.py | 4 +- tests/client/test_elicitation.py | 4 +- tests/server/auth/providers/test_azure.py | 41 ++++++++++++++++- tests/server/auth/providers/test_discord.py | 13 +++++- tests/server/auth/providers/test_github.py | 13 +++++- tests/server/auth/providers/test_google.py | 16 ++++++- tests/server/auth/providers/test_workos.py | 13 +++++- tests/server/auth/test_oauth_consent_flow.py | 6 --- tests/server/auth/test_oauth_proxy.py | 4 +- tests/server/tasks/test_task_dependencies.py | 2 +- tests/server/test_dependencies.py | 31 ++++++++----- tests/utilities/test_components.py | 2 +- uv.lock | 40 ++++++++--------- 34 files changed, 286 insertions(+), 101 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c58a64632..bb1274f34 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,7 +14,7 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.12.1 + rev: v0.14.10 hooks: # Run the linter. - id: ruff-check diff --git a/pyproject.toml b/pyproject.toml index 448b65f21..5d3e1cda4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -73,7 +73,7 @@ dev = [ "pytest-timeout>=2.4.0", "pytest-xdist>=3.6.1", "ruff>=0.12.8", - "ty==0.0.1a31", + "ty==0.0.5", "prek>=0.2.12", ] @@ -137,13 +137,6 @@ exclude = ["**/node_modules", "**/__pycache__", ".venv", ".git", "dist"] python-version = "3.10" [tool.ty.rules] -# Rules with too many errors to fix right now (40+ each) -no-matching-overload = "ignore" # 126 errors -unknown-argument = "ignore" # 61 errors - -# Rules with moderate errors that need more investigation -call-non-callable = "ignore" # 7 errors - # NOTE: ty currently doesn't support type narrowing with isinstance() on unions # See: https://github.com/astral-sh/ty/issues/122 and https://github.com/astral-sh/ty/issues/1113 # Some code uses `# ty: ignore[invalid-argument-type]` for this limitation. @@ -179,5 +172,18 @@ extend-select = [ "SIM", # flake8-simplify ] +[tool.basedpyright] +pythonVersion = "3.10" +typeCheckingMode = "standard" +reportMissingTypeStubs = false +reportUnknownParameterType = false +reportUnknownArgumentType = false +reportUnknownMemberType = false +reportUnknownVariableType = false +reportPrivateUsage = false +reportUnnecessaryIsInstance = false +reportUnnecessaryComparison = false +reportConstantRedefinition = false + [tool.codespell] ignore-words-list = "asend,shttp,te" diff --git a/src/fastmcp/client/client.py b/src/fastmcp/client/client.py index 28f282db7..a0f12139c 100644 --- a/src/fastmcp/client/client.py +++ b/src/fastmcp/client/client.py @@ -1573,7 +1573,7 @@ class Client(Generic[ClientTransportT]): McpError: If the request results in a TimeoutError | JSONRPCError """ # Send protocol request - params = PaginatedRequestParams(cursor=cursor, limit=limit) + params = PaginatedRequestParams(cursor=cursor, limit=limit) # type: ignore[call-arg] # Optional field in MCP SDK request = ListTasksRequest(params=params) server_response = await self.session.send_request( request=request, # type: ignore[invalid-argument-type] diff --git a/src/fastmcp/client/elicitation.py b/src/fastmcp/client/elicitation.py index 40a5edd8c..8df76c5b0 100644 --- a/src/fastmcp/client/elicitation.py +++ b/src/fastmcp/client/elicitation.py @@ -66,7 +66,7 @@ def create_elicitation_callback( f"{result.content!r}" ) return MCPElicitResult( - _meta=result.meta, + _meta=result.meta, # type: ignore[call-arg] # _meta is Pydantic alias for meta field action=result.action, content=content, ) diff --git a/src/fastmcp/client/tasks.py b/src/fastmcp/client/tasks.py index 54a397dad..c5d620cd5 100644 --- a/src/fastmcp/client/tasks.py +++ b/src/fastmcp/client/tasks.py @@ -379,7 +379,7 @@ class ToolTask(Task["CallToolResult"]): mcp_result = mcp.types.CallToolResult( content=raw_result.content, structuredContent=raw_result.structured_content, # type: ignore[arg-type] - _meta=raw_result.meta, + _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, mcp_result, raise_on_error=True diff --git a/src/fastmcp/prompts/prompt.py b/src/fastmcp/prompts/prompt.py index cef1d23e2..d2369ae58 100644 --- a/src/fastmcp/prompts/prompt.py +++ b/src/fastmcp/prompts/prompt.py @@ -111,7 +111,7 @@ class PromptResult(FastMCPBaseModel): return GetPromptResult( description=self.description, messages=self.messages, - _meta=self.meta, + _meta=self.meta, # type: ignore[call-arg] # _meta is Pydantic alias for meta field ) @@ -160,7 +160,7 @@ class Prompt(FastMCPComponent): arguments=arguments, title=overrides.get("title", self.title), icons=overrides.get("icons", self.icons), - _meta=overrides.get( + _meta=overrides.get( # type: ignore[call-arg] # _meta is Pydantic alias for meta field "_meta", self.get_meta(include_fastmcp_meta=include_fastmcp_meta) ), ) diff --git a/src/fastmcp/resources/resource.py b/src/fastmcp/resources/resource.py index a0ed8f192..3155bdfd7 100644 --- a/src/fastmcp/resources/resource.py +++ b/src/fastmcp/resources/resource.py @@ -123,14 +123,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, + _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, + _meta=self.meta, # type: ignore[call-arg] # _meta is Pydantic alias for meta field ) @@ -290,7 +290,7 @@ class Resource(FastMCPComponent): title=overrides.get("title", self.title), icons=overrides.get("icons", self.icons), annotations=overrides.get("annotations", self.annotations), - _meta=overrides.get( + _meta=overrides.get( # type: ignore[call-arg] # _meta is Pydantic alias for meta field "_meta", self.get_meta(include_fastmcp_meta=include_fastmcp_meta) ), ) diff --git a/src/fastmcp/resources/template.py b/src/fastmcp/resources/template.py index 9d3bbe89e..8de57c938 100644 --- a/src/fastmcp/resources/template.py +++ b/src/fastmcp/resources/template.py @@ -245,7 +245,7 @@ class ResourceTemplate(FastMCPComponent): title=overrides.get("title", self.title), icons=overrides.get("icons", self.icons), annotations=overrides.get("annotations", self.annotations), - _meta=overrides.get( + _meta=overrides.get( # type: ignore[call-arg] # _meta is Pydantic alias for meta field "_meta", self.get_meta(include_fastmcp_meta=include_fastmcp_meta) ), ) diff --git a/src/fastmcp/server/auth/oauth_proxy.py b/src/fastmcp/server/auth/oauth_proxy.py index b802754f0..9968040e3 100644 --- a/src/fastmcp/server/auth/oauth_proxy.py +++ b/src/fastmcp/server/auth/oauth_proxy.py @@ -1021,7 +1021,7 @@ class OAuthProxy(OAuthProvider): # Store transaction data for IdP callback processing if client.client_id is None: raise AuthorizeError( - error="invalid_client", # type: ignore[arg-type] + error="invalid_client", # type: ignore[arg-type] # "invalid_client" is valid OAuth error but not in Literal type error_description="Client ID is required", ) transaction = OAuthTransaction( @@ -1104,7 +1104,7 @@ class OAuthProxy(OAuthProvider): # Create authorization code object with PKCE challenge if client.client_id is None: raise AuthorizeError( - error="invalid_client", # type: ignore[arg-type] + error="invalid_client", # type: ignore[arg-type] # "invalid_client" is valid OAuth error but not in Literal type error_description="Client ID is required", ) return AuthorizationCode( @@ -1393,7 +1393,7 @@ class OAuthProxy(OAuthProvider): try: logger.debug("Refreshing upstream token (jti=%s)", refresh_jti[:8]) - token_response: dict[str, Any] = await oauth_client.refresh_token( # type: ignore[misc] + token_response: dict[str, Any] = await oauth_client.refresh_token( # type: ignore[assignment] url=self._upstream_token_endpoint, refresh_token=upstream_token_set.refresh_token, scope=" ".join(upstream_scopes) if upstream_scopes else None, @@ -1802,7 +1802,7 @@ class OAuthProxy(OAuthProvider): idp_tokens: dict[str, Any] = await oauth_client.fetch_token( **token_params - ) # type: ignore[misc] + ) # type: ignore[assignment] logger.debug( f"Successfully exchanged IdP code for tokens (transaction: {txn_id}, PKCE: {bool(proxy_code_verifier)})" diff --git a/src/fastmcp/server/auth/providers/aws.py b/src/fastmcp/server/auth/providers/aws.py index 49bdad4d5..6a1a4b67f 100644 --- a/src/fastmcp/server/auth/providers/aws.py +++ b/src/fastmcp/server/auth/providers/aws.py @@ -197,6 +197,10 @@ class AWSCognitoProvider(OIDCProxy): raise ValueError( "client_secret is required - set via parameter or FASTMCP_SERVER_AUTH_AWS_COGNITO_CLIENT_SECRET" ) + if not settings.base_url: + raise ValueError( + "base_url is required - set via parameter or FASTMCP_SERVER_AUTH_AWS_COGNITO_BASE_URL" + ) # Apply defaults required_scopes_final = settings.required_scopes or ["openid"] diff --git a/src/fastmcp/server/auth/providers/azure.py b/src/fastmcp/server/auth/providers/azure.py index 591919fa8..8c08a487c 100644 --- a/src/fastmcp/server/auth/providers/azure.py +++ b/src/fastmcp/server/auth/providers/azure.py @@ -233,6 +233,11 @@ class AzureProvider(OAuthProxy): ) raise ValueError(msg) + if not settings.base_url: + raise ValueError( + "base_url is required - set via parameter or FASTMCP_SERVER_AUTH_AZURE_BASE_URL" + ) + # Apply defaults self.identifier_uri = settings.identifier_uri or f"api://{settings.client_id}" self.additional_authorize_scopes = settings.additional_authorize_scopes or [] diff --git a/src/fastmcp/server/auth/providers/discord.py b/src/fastmcp/server/auth/providers/discord.py index f92a378bc..9e8d59d0b 100644 --- a/src/fastmcp/server/auth/providers/discord.py +++ b/src/fastmcp/server/auth/providers/discord.py @@ -267,6 +267,10 @@ class DiscordProvider(OAuthProxy): raise ValueError( "client_secret is required - set via parameter or FASTMCP_SERVER_AUTH_DISCORD_CLIENT_SECRET" ) + if not settings.base_url: + raise ValueError( + "base_url is required - set via parameter or FASTMCP_SERVER_AUTH_DISCORD_BASE_URL" + ) # Apply defaults timeout_seconds_final = settings.timeout_seconds or 10 diff --git a/src/fastmcp/server/auth/providers/github.py b/src/fastmcp/server/auth/providers/github.py index 0721297b2..04613f254 100644 --- a/src/fastmcp/server/auth/providers/github.py +++ b/src/fastmcp/server/auth/providers/github.py @@ -262,6 +262,10 @@ class GitHubProvider(OAuthProxy): raise ValueError( "client_secret is required - set via parameter or FASTMCP_SERVER_AUTH_GITHUB_CLIENT_SECRET" ) + if not settings.base_url: + raise ValueError( + "base_url is required - set via parameter or FASTMCP_SERVER_AUTH_GITHUB_BASE_URL" + ) # Apply defaults diff --git a/src/fastmcp/server/auth/providers/google.py b/src/fastmcp/server/auth/providers/google.py index 0f394e869..2ab06c110 100644 --- a/src/fastmcp/server/auth/providers/google.py +++ b/src/fastmcp/server/auth/providers/google.py @@ -286,6 +286,10 @@ class GoogleProvider(OAuthProxy): raise ValueError( "client_secret is required - set via parameter or FASTMCP_SERVER_AUTH_GOOGLE_CLIENT_SECRET" ) + if not settings.base_url: + raise ValueError( + "base_url is required - set via parameter or FASTMCP_SERVER_AUTH_GOOGLE_BASE_URL" + ) # Apply defaults timeout_seconds_final = settings.timeout_seconds or 10 diff --git a/src/fastmcp/server/auth/providers/workos.py b/src/fastmcp/server/auth/providers/workos.py index 14b6a55a3..7f7d20b2c 100644 --- a/src/fastmcp/server/auth/providers/workos.py +++ b/src/fastmcp/server/auth/providers/workos.py @@ -234,6 +234,10 @@ class WorkOSProvider(OAuthProxy): raise ValueError( "authkit_domain is required - set via parameter or FASTMCP_SERVER_AUTH_WORKOS_AUTHKIT_DOMAIN" ) + if not settings.base_url: + raise ValueError( + "base_url is required - set via parameter or FASTMCP_SERVER_AUTH_WORKOS_BASE_URL" + ) # Apply defaults and ensure authkit_domain is a full URL authkit_domain_str = settings.authkit_domain diff --git a/src/fastmcp/server/context.py b/src/fastmcp/server/context.py index 0e17ebf9f..346ded273 100644 --- a/src/fastmcp/server/context.py +++ b/src/fastmcp/server/context.py @@ -680,7 +680,7 @@ class Context: if mask_error_details is not None else settings.mask_error_details ) - tool_results = await run_sampling_tools( + tool_results: list[SamplingMessageContentBlock] = await run_sampling_tools( # type: ignore[assignment] step_tool_calls, tool_map, mask_error_details=effective_mask ) @@ -688,7 +688,7 @@ class Context: current_messages.append( SamplingMessage( role="user", - content=tool_results, # type: ignore[arg-type] + content=tool_results, ) ) @@ -912,10 +912,50 @@ class Context: """When response_type is a list of strings, the accepted elicitation will contain the selected string response""" + @overload async def elicit( self, message: str, - response_type: type[T] | list[str] | dict[str, dict[str, str]] | None = None, + response_type: dict[str, dict[str, str]], + ) -> AcceptedElicitation[str] | DeclinedElicitation | CancelledElicitation: ... + + """When response_type is a dict mapping keys to title dicts, the accepted + elicitation will contain the selected key""" + + @overload + async def elicit( + self, + message: str, + response_type: list[list[str]], + ) -> ( + AcceptedElicitation[list[str]] | DeclinedElicitation | CancelledElicitation + ): ... + + """When response_type is a list containing a list of strings (multi-select), + the accepted elicitation will contain a list of selected strings""" + + @overload + async def elicit( + self, + message: str, + response_type: list[dict[str, dict[str, str]]], + ) -> ( + AcceptedElicitation[list[str]] | DeclinedElicitation | CancelledElicitation + ): ... + + """When response_type is a list containing a dict mapping keys to title dicts + (multi-select with titles), the accepted elicitation will contain a list of + selected keys""" + + async def elicit( + self, + message: str, + response_type: type[T] + | list[str] + | dict[str, dict[str, str]] + | list[list[str]] + | list[dict[str, dict[str, str]]] + | None = None, ) -> ( AcceptedElicitation[T] | AcceptedElicitation[dict[str, Any]] diff --git a/src/fastmcp/server/dependencies.py b/src/fastmcp/server/dependencies.py index 545c1f019..b942c19bd 100644 --- a/src/fastmcp/server/dependencies.py +++ b/src/fastmcp/server/dependencies.py @@ -637,7 +637,7 @@ def get_access_token() -> AccessToken | None: scopes=access_token_as_dict["scopes"], # Optional fields expires_at=access_token_as_dict.get("expires_at"), - resource_owner=access_token_as_dict.get("resource_owner"), + resource_owner=access_token_as_dict.get("resource_owner"), # type: ignore[call-arg] # Optional field in MCP SDK claims=access_token_as_dict.get("claims"), ) except Exception as e: diff --git a/src/fastmcp/server/middleware/caching.py b/src/fastmcp/server/middleware/caching.py index 14b0c6c55..b9a08b539 100644 --- a/src/fastmcp/server/middleware/caching.py +++ b/src/fastmcp/server/middleware/caching.py @@ -194,21 +194,23 @@ class ResponseCachingMiddleware(Middleware): call_tool_settings or CallToolSettings() ) + # PydanticAdapter type signature will be fixed to accept generic aliases + # See: https://github.com/strawgate/py-key-value/pull/250 self._list_tools_cache: PydanticAdapter[list[Tool]] = PydanticAdapter( key_value=self._stats, - pydantic_model=list[Tool], + pydantic_model=list[Tool], # type: ignore[arg-type] default_collection="tools/list", ) self._list_resources_cache: PydanticAdapter[list[Resource]] = PydanticAdapter( key_value=self._stats, - pydantic_model=list[Resource], + pydantic_model=list[Resource], # type: ignore[arg-type] default_collection="resources/list", ) self._list_prompts_cache: PydanticAdapter[list[Prompt]] = PydanticAdapter( key_value=self._stats, - pydantic_model=list[Prompt], + pydantic_model=list[Prompt], # type: ignore[arg-type] default_collection="prompts/list", ) @@ -216,19 +218,19 @@ class ResponseCachingMiddleware(Middleware): list[CachableReadResourceContents] ] = PydanticAdapter( key_value=self._stats, - pydantic_model=list[CachableReadResourceContents], + pydantic_model=list[CachableReadResourceContents], # type: ignore[arg-type] default_collection="resources/read", ) self._get_prompt_cache: PydanticAdapter[PromptResult] = PydanticAdapter( key_value=self._stats, - pydantic_model=PromptResult, + pydantic_model=PromptResult, # type: ignore[arg-type] default_collection="prompts/get", ) self._call_tool_cache: PydanticAdapter[CachableToolResult] = PydanticAdapter( key_value=self._stats, - pydantic_model=CachableToolResult, + pydantic_model=CachableToolResult, # type: ignore[arg-type] default_collection="tools/call", ) diff --git a/src/fastmcp/server/server.py b/src/fastmcp/server/server.py index 8ff0903b1..28914943b 100644 --- a/src/fastmcp/server/server.py +++ b/src/fastmcp/server/server.py @@ -246,11 +246,14 @@ class FastMCP(Generic[LifespanResultT]): # if auth is `NotSet`, try to create a provider from the environment if auth is NotSet: if fastmcp.settings.server_auth is not None: - # server_auth_class returns the class itself - auth = fastmcp.settings.server_auth_class() + # server_auth_class returns the class itself, not an instance + auth_class = cast( + type[AuthProvider], fastmcp.settings.server_auth_class + ) + auth = auth_class() else: auth = None - self.auth: AuthProvider | None = cast(AuthProvider | None, auth) + self.auth: AuthProvider | None = auth if tools: for tool in tools: @@ -1859,7 +1862,11 @@ class FastMCP(Generic[LifespanResultT]): meta: dict[str, Any] | None = None, enabled: bool | None = None, task: bool | TaskConfig | None = None, - ) -> Callable[[AnyFunction], FunctionTool] | FunctionTool: + ) -> ( + Callable[[AnyFunction], FunctionTool] + | FunctionTool + | partial[Callable[[AnyFunction], FunctionTool] | FunctionTool] + ): """Decorator to register a tool. Tools can optionally request a Context object by adding a parameter with the @@ -2241,7 +2248,11 @@ class FastMCP(Generic[LifespanResultT]): enabled: bool | None = None, meta: dict[str, Any] | None = None, task: bool | TaskConfig | None = None, - ) -> Callable[[AnyFunction], FunctionPrompt] | FunctionPrompt: + ) -> ( + Callable[[AnyFunction], FunctionPrompt] + | FunctionPrompt + | partial[Callable[[AnyFunction], FunctionPrompt] | FunctionPrompt] + ): """Decorator to register a prompt. Prompts can optionally request a Context object by adding a parameter with the @@ -2490,7 +2501,7 @@ class FastMCP(Generic[LifespanResultT]): async with self._lifespan_manager(): config = uvicorn.Config(app, host=host, port=port, **config_kwargs) server = uvicorn.Server(config) - path = app.state.path.lstrip("/") # type: ignore + path = getattr(app.state, "path", "").lstrip("/") logger.info( f"Starting MCP server {self.name!r} with transport {transport!r} on http://{host}:{port}/{path}" ) @@ -2897,7 +2908,8 @@ class FastMCP(Generic[LifespanResultT]): client_factory = fresh_client_factory else: - base_client = ProxyClient(backend) # type: ignore + # backend is not a Client, so it's compatible with ProxyClient.__init__ + base_client = ProxyClient(cast(Any, backend)) # Fresh client created from transport - use fresh sessions per request def proxy_client_factory(): diff --git a/src/fastmcp/server/tasks/handlers.py b/src/fastmcp/server/tasks/handlers.py index fc2254701..cf6573a5a 100644 --- a/src/fastmcp/server/tasks/handlers.py +++ b/src/fastmcp/server/tasks/handlers.py @@ -96,7 +96,7 @@ async def submit_to_docket( jsonrpc="2.0", method="notifications/tasks/created", params={}, # Empty params per spec - _meta={ # taskId in _meta per spec + _meta={ # type: ignore[call-arg] # _meta is Pydantic alias for meta field "modelcontextprotocol.io/related-task": { "taskId": server_task_id, } diff --git a/src/fastmcp/server/tasks/requests.py b/src/fastmcp/server/tasks/requests.py index 8c7165ebe..d9aee6147 100644 --- a/src/fastmcp/server/tasks/requests.py +++ b/src/fastmcp/server/tasks/requests.py @@ -7,7 +7,7 @@ These handlers query and manage existing tasks (contrast with handlers.py which from __future__ import annotations from datetime import datetime, timedelta, timezone -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, Literal import mcp.types from docket.execution import ExecutionState @@ -145,7 +145,9 @@ async def tasks_get_handler(server: FastMCP, params: dict[str, Any]) -> GetTaskR await execution.sync() # Map Docket state to MCP state - mcp_state = DOCKET_TO_MCP_STATE.get(execution.state, "failed") + mcp_state: Literal[ + "working", "input_required", "completed", "failed", "cancelled" + ] = DOCKET_TO_MCP_STATE.get(execution.state, "failed") # type: ignore[assignment] # Build response (use default ttl since we don't track per-task values) # createdAt is REQUIRED per SEP-1686 final spec (line 430) @@ -163,10 +165,22 @@ async def tasks_get_handler(server: FastMCP, params: dict[str, Any]) -> GetTaskR # Extract progress message from Docket if available (spec line 403) status_message = execution.progress.message + # createdAt is required per spec, but can be None from Redis + # Parse ISO string to datetime, or use current time as fallback + if created_at: + try: + created_at_dt = datetime.fromisoformat( + created_at.replace("Z", "+00:00") + ) + except (ValueError, AttributeError): + created_at_dt = datetime.now(timezone.utc) + else: + created_at_dt = datetime.now(timezone.utc) + return GetTaskResult( taskId=client_task_id, - status=mcp_state, # type: ignore[arg-type] - createdAt=created_at, # type: ignore[arg-type] + status=mcp_state, + createdAt=created_at_dt, lastUpdatedAt=datetime.now(timezone.utc), ttl=DEFAULT_TTL_MS, pollInterval=poll_interval_ms, @@ -255,7 +269,7 @@ async def tasks_result_handler(server: FastMCP, params: dict[str, Any]) -> Any: return mcp.types.CallToolResult( content=[mcp.types.TextContent(type="text", text=str(error))], isError=True, - _meta={ + _meta={ # type: ignore[call-arg] # _meta is Pydantic alias for meta field "modelcontextprotocol.io/related-task": { "taskId": client_task_id, } @@ -287,11 +301,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, + _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 + content=mcp_result, + _meta=related_task_meta, # type: ignore[call-arg] # _meta is Pydantic alias for meta field ) return mcp_result @@ -308,7 +323,7 @@ async def tasks_result_handler(server: FastMCP, params: dict[str, Any]) -> Any: mcp_content = resource_content.to_mcp_resource_contents(component_id) return mcp.types.ReadResourceResult( contents=[mcp_content], - _meta=related_task_meta, + _meta=related_task_meta, # type: ignore[call-arg] # _meta is Pydantic alias for meta field ) elif task_type == "template": @@ -317,7 +332,7 @@ async def tasks_result_handler(server: FastMCP, params: dict[str, Any]) -> Any: mcp_content = resource_content.to_mcp_resource_contents(component_id) return mcp.types.ReadResourceResult( contents=[mcp_content], - _meta=related_task_meta, + _meta=related_task_meta, # type: ignore[call-arg] # _meta is Pydantic alias for meta field ) else: diff --git a/src/fastmcp/tools/tool.py b/src/fastmcp/tools/tool.py index 4ca32c765..a2a6f431e 100644 --- a/src/fastmcp/tools/tool.py +++ b/src/fastmcp/tools/tool.py @@ -116,7 +116,7 @@ class ToolResult: return CallToolResult( structuredContent=self.structured_content, content=self.content, - _meta=self.meta, + _meta=self.meta, # type: ignore[call-arg] # _meta is Pydantic alias for meta field ) if self.structured_content is None: return self.content @@ -190,7 +190,7 @@ class Tool(FastMCPComponent): icons=overrides.get("icons", self.icons), annotations=overrides.get("annotations", self.annotations), execution=overrides.get("execution", self.execution), - _meta=overrides.get( + _meta=overrides.get( # type: ignore[call-arg] # _meta is Pydantic alias for meta field "_meta", self.get_meta(include_fastmcp_meta=include_fastmcp_meta) ), ) diff --git a/tests/client/test_elicitation.py b/tests/client/test_elicitation.py index 360743628..19567a61d 100644 --- a/tests/client/test_elicitation.py +++ b/tests/client/test_elicitation.py @@ -805,7 +805,7 @@ async def test_list_list_multi_select_untitled(): if result.action == "accept": assert isinstance(result, AcceptedElicitation) assert isinstance(result.data, list) - return ",".join(result.data) + return ",".join(result.data) # type: ignore[no-matching-overload] return "declined" async def elicitation_handler(message, response_type, params, ctx): @@ -843,7 +843,7 @@ async def test_list_dict_multi_select_titled(): if result.action == "accept": assert isinstance(result, AcceptedElicitation) assert isinstance(result.data, list) - return ",".join(result.data) + return ",".join(result.data) # type: ignore[no-matching-overload] return "declined" async def elicitation_handler(message, response_type, params, ctx): diff --git a/tests/server/auth/providers/test_azure.py b/tests/server/auth/providers/test_azure.py index 72fb398eb..c794a6807 100644 --- a/tests/server/auth/providers/test_azure.py +++ b/tests/server/auth/providers/test_azure.py @@ -133,18 +133,30 @@ class TestAzureProvider: required_scopes=[], ) + def test_init_missing_base_url_raises_error(self): + """Test that missing base_url raises ValueError.""" + with patch.dict(os.environ, {}, clear=True): + with pytest.raises(ValueError, match="base_url is required"): + AzureProvider( + client_id="test_client", + client_secret="test_secret", + tenant_id="test-tenant", + required_scopes=["read"], + jwt_signing_key="test-secret", + ) + def test_init_defaults(self): """Test that default values are applied correctly.""" provider = AzureProvider( client_id="test_client", client_secret="test_secret", tenant_id="test-tenant", + base_url="https://myserver.com", required_scopes=["read"], jwt_signing_key="test-secret", ) # Check defaults - assert provider.base_url is None assert provider._redirect_path == "/auth/callback" # Azure provider defaults are set but we can't easily verify them without accessing internals @@ -179,6 +191,7 @@ class TestAzureProvider: client_id="test_client", client_secret="test_secret", tenant_id="organizations", + base_url="https://myserver.com", required_scopes=["read"], jwt_signing_key="test-secret", ) @@ -190,6 +203,7 @@ class TestAzureProvider: client_id="test_client", client_secret="test_secret", tenant_id="consumers", + base_url="https://myserver.com", required_scopes=["read"], jwt_signing_key="test-secret", ) @@ -203,6 +217,7 @@ class TestAzureProvider: client_id="test_client", client_secret="test_secret", tenant_id="test-tenant", + base_url="https://myserver.com", required_scopes=[ "read", "write", @@ -227,6 +242,7 @@ class TestAzureProvider: client_id="test_client", client_secret="test_secret", tenant_id="test-tenant", + base_url="https://myserver.com", required_scopes=["read"], jwt_signing_key="test-secret", ) @@ -240,6 +256,7 @@ class TestAzureProvider: client_id="test_client", client_secret="test_secret", tenant_id="my-tenant", + base_url="https://myserver.com", identifier_uri="api://my-api", required_scopes=[".default"], jwt_signing_key="test-secret", @@ -393,6 +410,7 @@ class TestAzureProvider: client_id="test_client", client_secret="test_secret", tenant_id="test-tenant", + base_url="https://myserver.com", required_scopes=["read"], jwt_signing_key="test-secret", ) @@ -421,6 +439,7 @@ class TestAzureProvider: client_id="test_client", client_secret="test_secret", tenant_id="gov-tenant-id", + base_url="https://myserver.com", required_scopes=["read"], base_authority="login.microsoftonline.us", jwt_signing_key="test-secret", @@ -452,6 +471,7 @@ class TestAzureProvider: "FASTMCP_SERVER_AUTH_AZURE_CLIENT_ID": "env-client-id", "FASTMCP_SERVER_AUTH_AZURE_CLIENT_SECRET": "env-secret", "FASTMCP_SERVER_AUTH_AZURE_TENANT_ID": "env-tenant-id", + "FASTMCP_SERVER_AUTH_AZURE_BASE_URL": "https://myserver.com", "FASTMCP_SERVER_AUTH_AZURE_REQUIRED_SCOPES": "read", "FASTMCP_SERVER_AUTH_AZURE_BASE_AUTHORITY": "login.microsoftonline.us", "FASTMCP_SERVER_AUTH_AZURE_JWT_SIGNING_KEY": "test-secret", @@ -483,6 +503,7 @@ class TestAzureProvider: client_id="test_client", client_secret="test_secret", tenant_id="organizations", + base_url="https://myserver.com", required_scopes=["read"], base_authority="login.microsoftonline.us", jwt_signing_key="test-secret", @@ -498,6 +519,7 @@ class TestAzureProvider: client_id="test_client", client_secret="test_secret", tenant_id="test-tenant", + base_url="https://myserver.com", identifier_uri="api://my-api", required_scopes=["read", "write"], jwt_signing_key="test-secret", @@ -516,6 +538,7 @@ class TestAzureProvider: client_id="test_client", client_secret="test_secret", tenant_id="test-tenant", + base_url="https://myserver.com", identifier_uri="api://my-api", required_scopes=["read"], jwt_signing_key="test-secret", @@ -536,6 +559,7 @@ class TestAzureProvider: client_id="test_client", client_secret="test_secret", tenant_id="test-tenant", + base_url="https://myserver.com", identifier_uri="api://my-api", required_scopes=["read"], additional_authorize_scopes=[ @@ -566,6 +590,7 @@ class TestAzureProvider: client_id="test_client", client_secret="test_secret", tenant_id="test-tenant", + base_url="https://myserver.com", identifier_uri="api://my-api", required_scopes=["read"], additional_authorize_scopes=["User.Read", "openid"], @@ -590,6 +615,7 @@ class TestAzureProvider: client_id="test_client", client_secret="test_secret", tenant_id="test-tenant", + base_url="https://myserver.com", identifier_uri="api://my-api", required_scopes=["read"], additional_authorize_scopes=["User.Read"], @@ -613,6 +639,7 @@ class TestAzureProvider: client_id="test_client", client_secret="test_secret", tenant_id="test-tenant", + base_url="https://myserver.com", identifier_uri="api://my-api", required_scopes=["read"], jwt_signing_key="test-secret", @@ -634,6 +661,7 @@ class TestAzureProvider: client_id="test_client", client_secret="test_secret", tenant_id="test-tenant", + base_url="https://myserver.com", identifier_uri="api://my-api", required_scopes=["read"], additional_authorize_scopes=["User.Read", "openid"], @@ -653,6 +681,7 @@ class TestAzureProvider: client_id="test_client", client_secret="test_secret", tenant_id="test-tenant", + base_url="https://myserver.com", identifier_uri="api://my-api", required_scopes=["read"], jwt_signing_key="test-secret", @@ -671,6 +700,7 @@ class TestAzureProvider: client_id="test_client", client_secret="test_secret", tenant_id="test-tenant", + base_url="https://myserver.com", identifier_uri="api://my-api", required_scopes=["read"], additional_authorize_scopes=["User.Read", "openid"], @@ -697,6 +727,7 @@ class TestAzureProvider: client_id="test_client", client_secret="test_secret", tenant_id="test-tenant", + base_url="https://myserver.com", identifier_uri="api://my-api", required_scopes=["read"], jwt_signing_key="test-secret", @@ -736,6 +767,7 @@ class TestOIDCScopeHandling: client_id="test_client", client_secret="test_secret", tenant_id="test-tenant", + base_url="https://myserver.com", identifier_uri="api://my-api", required_scopes=["read"], jwt_signing_key="test-secret", @@ -754,6 +786,7 @@ class TestOIDCScopeHandling: client_id="test_client", client_secret="test_secret", tenant_id="test-tenant", + base_url="https://myserver.com", identifier_uri="api://my-api", required_scopes=["read"], jwt_signing_key="test-secret", @@ -778,6 +811,7 @@ class TestOIDCScopeHandling: client_id="test_client", client_secret="test_secret", tenant_id="test-tenant", + base_url="https://myserver.com", identifier_uri="api://my-api", required_scopes=["read"], jwt_signing_key="test-secret", @@ -795,6 +829,7 @@ class TestOIDCScopeHandling: client_id="test_client", client_secret="test_secret", tenant_id="test-tenant", + base_url="https://myserver.com", identifier_uri="api://my-api", required_scopes=["read"], jwt_signing_key="test-secret", @@ -819,6 +854,7 @@ class TestOIDCScopeHandling: client_id="test_client", client_secret="test_secret", tenant_id="test-tenant", + base_url="https://myserver.com", identifier_uri="api://my-api", required_scopes=["read", "openid", "profile"], jwt_signing_key="test-secret", @@ -833,6 +869,7 @@ class TestOIDCScopeHandling: client_id="test_client", client_secret="test_secret", tenant_id="test-tenant", + base_url="https://myserver.com", identifier_uri="api://my-api", required_scopes=["openid", "profile"], jwt_signing_key="test-secret", @@ -847,6 +884,7 @@ class TestOIDCScopeHandling: client_id="test_client", client_secret="test_secret", tenant_id="test-tenant", + base_url="https://myserver.com", identifier_uri="api://my-api", required_scopes=["read", "openid", "profile"], jwt_signing_key="test-secret", @@ -868,6 +906,7 @@ class TestOIDCScopeHandling: client_id="test_client", client_secret="test_secret", tenant_id="test-tenant", + base_url="https://myserver.com", identifier_uri="api://my-api", required_scopes=["read"], jwt_signing_key="test-secret", diff --git a/tests/server/auth/providers/test_discord.py b/tests/server/auth/providers/test_discord.py index ee07e6c60..227714d84 100644 --- a/tests/server/auth/providers/test_discord.py +++ b/tests/server/auth/providers/test_discord.py @@ -70,16 +70,26 @@ class TestDiscordProvider: with pytest.raises(ValueError, match="client_secret is required"): DiscordProvider(client_id="env_client_id") + def test_init_missing_base_url_raises_error(self): + """Test that missing base_url raises ValueError.""" + with patch.dict(os.environ, {}, clear=True): + with pytest.raises(ValueError, match="base_url is required"): + DiscordProvider( + client_id="env_client_id", + client_secret="GOCSPX-test123", + jwt_signing_key="test-secret", + ) + def test_init_defaults(self): """Test that default values are applied correctly.""" provider = DiscordProvider( client_id="env_client_id", client_secret="GOCSPX-test123", + base_url="https://myserver.com", jwt_signing_key="test-secret", ) # Check defaults - assert provider.base_url is None assert provider._redirect_path == "/auth/callback" def test_oauth_endpoints_configured_correctly(self): @@ -108,6 +118,7 @@ class TestDiscordProvider: provider = DiscordProvider( client_id="env_client_id", client_secret="GOCSPX-test123", + base_url="https://myserver.com", required_scopes=[ "identify", "email", diff --git a/tests/server/auth/providers/test_github.py b/tests/server/auth/providers/test_github.py index 21789446c..07853e21b 100644 --- a/tests/server/auth/providers/test_github.py +++ b/tests/server/auth/providers/test_github.py @@ -119,6 +119,7 @@ class TestGitHubProvider: { "FASTMCP_SERVER_AUTH_GITHUB_CLIENT_ID": "env_client_id", "FASTMCP_SERVER_AUTH_GITHUB_CLIENT_SECRET": "env_secret", + "FASTMCP_SERVER_AUTH_GITHUB_BASE_URL": "https://env-example.com", "FASTMCP_SERVER_AUTH_GITHUB_JWT_SIGNING_KEY": "test-secret", }, ): @@ -147,16 +148,26 @@ class TestGitHubProvider: with pytest.raises(ValueError, match="client_secret is required"): GitHubProvider(client_id="test_client") + def test_init_missing_base_url_raises_error(self): + """Test that missing base_url raises ValueError.""" + with patch.dict(os.environ, {}, clear=True): + with pytest.raises(ValueError, match="base_url is required"): + GitHubProvider( + client_id="test_client", + client_secret="test_secret", + jwt_signing_key="test-secret", + ) + def test_init_defaults(self): """Test that default values are applied correctly.""" provider = GitHubProvider( client_id="test_client", client_secret="test_secret", + base_url="https://example.com", jwt_signing_key="test-secret", ) # Check defaults - assert provider.base_url is None assert provider._redirect_path == "/auth/callback" # The required_scopes should be passed to the token verifier assert provider._token_validator.required_scopes == ["user"] diff --git a/tests/server/auth/providers/test_google.py b/tests/server/auth/providers/test_google.py index 66d4aa8c3..73d76548c 100644 --- a/tests/server/auth/providers/test_google.py +++ b/tests/server/auth/providers/test_google.py @@ -70,16 +70,26 @@ class TestGoogleProvider: with pytest.raises(ValueError, match="client_secret is required"): GoogleProvider(client_id="123456789.apps.googleusercontent.com") + def test_init_missing_base_url_raises_error(self): + """Test that missing base_url raises ValueError.""" + with patch.dict(os.environ, {}, clear=True): + with pytest.raises(ValueError, match="base_url is required"): + GoogleProvider( + client_id="123456789.apps.googleusercontent.com", + client_secret="GOCSPX-test123", + jwt_signing_key="test-secret", + ) + def test_init_defaults(self): """Test that default values are applied correctly.""" provider = GoogleProvider( client_id="123456789.apps.googleusercontent.com", client_secret="GOCSPX-test123", + base_url="https://myserver.com", jwt_signing_key="test-secret", ) # Check defaults - assert provider.base_url is None assert provider._redirect_path == "/auth/callback" # Google provider has ["openid"] as default but we can't easily verify without accessing internals @@ -109,6 +119,7 @@ class TestGoogleProvider: provider = GoogleProvider( client_id="123456789.apps.googleusercontent.com", client_secret="GOCSPX-test123", + base_url="https://myserver.com", required_scopes=[ "openid", "https://www.googleapis.com/auth/userinfo.email", @@ -125,6 +136,7 @@ class TestGoogleProvider: provider = GoogleProvider( client_id="123456789.apps.googleusercontent.com", client_secret="GOCSPX-test123", + base_url="https://myserver.com", jwt_signing_key="test-secret", ) @@ -139,6 +151,7 @@ class TestGoogleProvider: provider = GoogleProvider( client_id="123456789.apps.googleusercontent.com", client_secret="GOCSPX-test123", + base_url="https://myserver.com", jwt_signing_key="test-secret", extra_authorize_params={"prompt": "select_account"}, ) @@ -153,6 +166,7 @@ class TestGoogleProvider: provider = GoogleProvider( client_id="123456789.apps.googleusercontent.com", client_secret="GOCSPX-test123", + base_url="https://myserver.com", jwt_signing_key="test-secret", extra_authorize_params={"login_hint": "user@example.com"}, ) diff --git a/tests/server/auth/providers/test_workos.py b/tests/server/auth/providers/test_workos.py index 4bff3c194..8d477645f 100644 --- a/tests/server/auth/providers/test_workos.py +++ b/tests/server/auth/providers/test_workos.py @@ -126,17 +126,28 @@ class TestWorkOSProvider: assert parsed.netloc == "localhost:8080" assert parsed.path == "/oauth2/authorize" + def test_init_missing_base_url_raises_error(self): + """Test that missing base_url raises ValueError.""" + with patch.dict(os.environ, {}, clear=True): + with pytest.raises(ValueError, match="base_url is required"): + WorkOSProvider( + client_id="test_client", + client_secret="test_secret", + authkit_domain="https://test.authkit.app", + jwt_signing_key="test-secret", + ) + def test_init_defaults(self): """Test that default values are applied correctly.""" provider = WorkOSProvider( client_id="test_client", client_secret="test_secret", authkit_domain="https://test.authkit.app", + base_url="https://myserver.com", jwt_signing_key="test-secret", ) # Check defaults - assert provider.base_url is None assert provider._redirect_path == "/auth/callback" # WorkOS provider has no default scopes but we can't easily verify without accessing internals diff --git a/tests/server/auth/test_oauth_consent_flow.py b/tests/server/auth/test_oauth_consent_flow.py index afe7eab80..87304935d 100644 --- a/tests/server/auth/test_oauth_consent_flow.py +++ b/tests/server/auth/test_oauth_consent_flow.py @@ -113,7 +113,6 @@ async def _start_flow( redirect_uri_provided_explicitly=True, state="client-state-xyz", code_challenge="challenge", - code_challenge_method="S256", scopes=["read"], ) consent_url = await proxy.authorize( @@ -155,7 +154,6 @@ class TestServerSideStorage: redirect_uri_provided_explicitly=True, state="client-state-123", code_challenge="challenge-abc", - code_challenge_method="S256", scopes=["read", "write"], ) @@ -208,7 +206,6 @@ class TestServerSideStorage: redirect_uri_provided_explicitly=True, state="client-state", code_challenge="challenge-xyz", - code_challenge_method="S256", scopes=["read"], ) @@ -288,7 +285,6 @@ class TestServerSideStorage: redirect_uri_provided_explicitly=True, state="test-state", code_challenge="test-challenge", - code_challenge_method="S256", scopes=["read"], ) @@ -485,7 +481,6 @@ class TestStoragePersistence: redirect_uri_provided_explicitly=True, state="persist-state", code_challenge="persist-challenge", - code_challenge_method="S256", scopes=["read"], ) @@ -523,7 +518,6 @@ class TestStoragePersistence: redirect_uri_provided_explicitly=True, state="pydantic-state", code_challenge="pydantic-challenge", - code_challenge_method="S256", scopes=["read", "write"], ) diff --git a/tests/server/auth/test_oauth_proxy.py b/tests/server/auth/test_oauth_proxy.py index 8c214ca20..f389c5f86 100644 --- a/tests/server/auth/test_oauth_proxy.py +++ b/tests/server/auth/test_oauth_proxy.py @@ -451,7 +451,7 @@ class TestOAuthProxyAuthorization: client_id="test-client", client_secret="test-secret", redirect_uris=[AnyUrl("http://localhost:54321/callback")], - jwt_signing_key="test-secret", + jwt_signing_key="test-secret", # type: ignore[call-arg] # Optional field in MCP SDK ) # Register client first (required for consent flow) @@ -462,7 +462,6 @@ class TestOAuthProxyAuthorization: redirect_uri_provided_explicitly=True, state="client-state-123", code_challenge="challenge-abc", - code_challenge_method="S256", scopes=["read", "write"], ) @@ -946,7 +945,6 @@ class TestOAuthProxyE2E: redirect_uri_provided_explicitly=True, state="client-state", code_challenge="client_challenge_value", - code_challenge_method="S256", scopes=["read"], ) diff --git a/tests/server/tasks/test_task_dependencies.py b/tests/server/tasks/test_task_dependencies.py index 8bc6d4622..5579fb560 100644 --- a/tests/server/tasks/test_task_dependencies.py +++ b/tests/server/tasks/test_task_dependencies.py @@ -228,7 +228,7 @@ async def test_dependency_context_managers_cleaned_up_in_background(): cleanup_called.append("exit") @mcp.tool(task=True) - async def use_connection(name: str, conn: str = Depends(tracked_connection)) -> str: + async def use_connection(name: str, conn: str = Depends(tracked_connection)) -> str: # type: ignore[assignment] assert conn == "connection" assert "enter" in cleanup_called assert "exit" not in cleanup_called # Still open during execution diff --git a/tests/server/test_dependencies.py b/tests/server/test_dependencies.py index 61522f461..330a89649 100644 --- a/tests/server/test_dependencies.py +++ b/tests/server/test_dependencies.py @@ -68,7 +68,7 @@ async def test_depends_with_async_function(mcp: FastMCP): return 42 @mcp.tool() - async def greet_user(name: str, user_id: int = Depends(get_user_id)) -> str: + async def greet_user(name: str, user_id: int = Depends(get_user_id)) -> str: # type: ignore[assignment] return f"Hello {name}, your ID is {user_id}" async with Client(mcp) as client: @@ -93,7 +93,7 @@ async def test_depends_with_async_context_manager(mcp: FastMCP): cleanup_called = True @mcp.tool() - async def query_db(sql: str, db: str = Depends(get_database)) -> str: + async def query_db(sql: str, db: str = Depends(get_database)) -> str: # type: ignore[assignment] return f"Executing '{sql}' on {db}" async with Client(mcp) as client: @@ -202,7 +202,7 @@ async def test_sync_tool_with_async_dependency(mcp: FastMCP): return "loaded_config" @mcp.tool() - def process_data(value: int, config: str = Depends(fetch_config)) -> str: + def process_data(value: int, config: str = Depends(fetch_config)) -> str: # type: ignore[assignment] return f"Processing {value} with {config}" async with Client(mcp) as client: @@ -392,7 +392,8 @@ async def test_async_tool_context_manager_stays_open(mcp: FastMCP): @mcp.tool() async def query_data( - query: str, connection: Connection = Depends(get_connection) + query: str, + connection: Connection = Depends(get_connection), # type: ignore[assignment] ) -> str: assert connection.is_open return f"open={connection.is_open}" @@ -408,7 +409,7 @@ async def test_async_resource_context_manager_stays_open(mcp: FastMCP): """Test that context manager dependencies stay open during async resource execution.""" @mcp.resource("data://config") - async def load_config(connection: Connection = Depends(get_connection)) -> str: + async def load_config(connection: Connection = Depends(get_connection)) -> str: # type: ignore[assignment] assert connection.is_open return f"open={connection.is_open}" @@ -424,7 +425,8 @@ async def test_async_resource_template_context_manager_stays_open(mcp: FastMCP): @mcp.resource("user://{user_id}") async def get_user( - user_id: str, connection: Connection = Depends(get_connection) + user_id: str, + connection: Connection = Depends(get_connection), # type: ignore[assignment] ) -> str: assert connection.is_open return f"open={connection.is_open},user={user_id}" @@ -441,7 +443,8 @@ async def test_async_prompt_context_manager_stays_open(mcp: FastMCP): @mcp.prompt() async def research_prompt( - topic: str, connection: Connection = Depends(get_connection) + topic: str, + connection: Connection = Depends(get_connection), # type: ignore[assignment] ) -> str: assert connection.is_open return f"open={connection.is_open},topic={topic}" @@ -484,7 +487,8 @@ async def test_connection_dependency_excluded_from_tool_schema(mcp: FastMCP): @mcp.tool() async def with_connection( - name: str, connection: Connection = Depends(get_connection) + name: str, + connection: Connection = Depends(get_connection), # type: ignore[assignment] ) -> str: return name @@ -509,7 +513,8 @@ async def test_sync_tool_context_manager_stays_open(mcp: FastMCP): @mcp.tool() async def query_sync( - query: str, connection: Connection = Depends(get_sync_connection) + query: str, + connection: Connection = Depends(get_sync_connection), # type: ignore[assignment] ) -> str: assert connection.is_open return f"open={connection.is_open}" @@ -535,7 +540,7 @@ async def test_sync_resource_context_manager_stays_open(mcp: FastMCP): conn.is_open = False @mcp.resource("data://sync") - async def load_sync(connection: Connection = Depends(get_sync_connection)) -> str: + async def load_sync(connection: Connection = Depends(get_sync_connection)) -> str: # type: ignore[assignment] assert connection.is_open return f"open={connection.is_open}" @@ -561,7 +566,8 @@ async def test_sync_resource_template_context_manager_stays_open(mcp: FastMCP): @mcp.resource("item://{item_id}") async def get_item( - item_id: str, connection: Connection = Depends(get_sync_connection) + item_id: str, + connection: Connection = Depends(get_sync_connection), # type: ignore[assignment] ) -> str: assert connection.is_open return f"open={connection.is_open},item={item_id}" @@ -588,7 +594,8 @@ async def test_sync_prompt_context_manager_stays_open(mcp: FastMCP): @mcp.prompt() async def sync_prompt( - topic: str, connection: Connection = Depends(get_sync_connection) + topic: str, + connection: Connection = Depends(get_sync_connection), # type: ignore[assignment] ) -> str: assert connection.is_open return f"open={connection.is_open},topic={topic}" diff --git a/tests/utilities/test_components.py b/tests/utilities/test_components.py index 330ab896a..15a9232a3 100644 --- a/tests/utilities/test_components.py +++ b/tests/utilities/test_components.py @@ -183,7 +183,7 @@ class TestFastMCPComponent: def test_extra_fields_forbidden(self): """Test that extra fields are not allowed.""" with pytest.raises(ValidationError) as exc_info: - FastMCPComponent(name="test", unknown_field="value") + FastMCPComponent(name="test", unknown_field="value") # type: ignore[call-arg] # Intentionally passing invalid field for test assert "Extra inputs are not permitted" in str(exc_info.value) diff --git a/uv.lock b/uv.lock index c77808e02..08c5f65c3 100644 --- a/uv.lock +++ b/uv.lock @@ -784,7 +784,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.1a31" }, + { name = "ty", specifier = "==0.0.5" }, ] [[package]] @@ -2521,27 +2521,27 @@ wheels = [ [[package]] name = "ty" -version = "0.0.1a31" +version = "0.0.5" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/30/78/daa1e70377b8127e06db63063b7dd9694cb2bb611b4e3c2182b9ec5a02a1/ty-0.0.1a31.tar.gz", hash = "sha256:b878b04af63b1e716436897838ca6a107a672539155b6fc2051268cd85da9cd6", size = 4656004, upload-time = "2025-12-04T09:01:47.147Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9e/db/6299d478000f4f1c6f9bf2af749359381610ffc4cbe6713b66e436ecf6e7/ty-0.0.5.tar.gz", hash = "sha256:983da6330773ff71e2b249810a19c689f9a0372f6e21bbf7cde37839d05b4346", size = 4806218, upload-time = "2025-12-20T21:19:17.24Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/08/4c/1e91d6b22dee1435db1cdf55e54ec601497dba650684517b1cd5b4345e80/ty-0.0.1a31-py3-none-linux_armv6l.whl", hash = "sha256:662b9a3a3497da12416789e21fda9eb4e1ac66c5233867d89953916099ee44f5", size = 9620261, upload-time = "2025-12-04T09:01:35.001Z" }, - { url = "https://files.pythonhosted.org/packages/94/8f/eb6ac56cc03a00d3258c3362c4fb5a58152d03e9fa207db0465e2dc717e2/ty-0.0.1a31-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:da05f73df587ff1362d487681370db47541123c005a0d1a60a5a048039e309cc", size = 9411370, upload-time = "2025-12-04T09:01:23.903Z" }, - { url = "https://files.pythonhosted.org/packages/d6/72/2cdbef5bd7ee7a58e71e67e845ae3f99dca695d0bca7561a3294fb8d723e/ty-0.0.1a31-py3-none-macosx_11_0_arm64.whl", hash = "sha256:74032bf207ce1eddc042f26aa9b6e0713373cf2c502174a53a41f9c469f02adb", size = 8925400, upload-time = "2025-12-04T09:01:59.074Z" }, - { url = "https://files.pythonhosted.org/packages/f4/4d/a10c3f2e8969e9e1efe3179d2c961236413c9765c9f95e84e8f515fb9b02/ty-0.0.1a31-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cd383fd54872df15816a7853a8c824400c85f850916bad2052564bad8462f4f2", size = 9201615, upload-time = "2025-12-04T09:01:21.085Z" }, - { url = "https://files.pythonhosted.org/packages/47/e5/bd26f0fc432459718b72a0bb41bd222fd1fad81c1d5f645a7eba94e14be6/ty-0.0.1a31-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c6defaf175bce7c91cea9a168a1c30bb523269ed174941cd31f8edc2d77f8ec7", size = 9401110, upload-time = "2025-12-04T09:01:32.241Z" }, - { url = "https://files.pythonhosted.org/packages/ca/63/cbb3419f74ce38c0a2affbc269d4d27ec032cfbc3b011a8db5815c89f540/ty-0.0.1a31-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2d41f7e68f05517177ef82d89bfe0bf8e787a6b72ad396c1e44a16ef353b95e2", size = 9779837, upload-time = "2025-12-04T09:01:37.55Z" }, - { url = "https://files.pythonhosted.org/packages/69/fb/1d99243a0e005fe8d53671d4a25d5ddcf345a12fb3c683726bd597e42f23/ty-0.0.1a31-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:22f2298a0de1a8af24f50e498023770b05ea4fc0ccebb2c53deb40ff73dc76fc", size = 10444412, upload-time = "2025-12-04T09:01:56.888Z" }, - { url = "https://files.pythonhosted.org/packages/65/7f/95242feb774356b7a93beb5278cd8c8bbb6a8b12d94977ff954929ed257e/ty-0.0.1a31-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:38669b9aa53bd87160a2ee8447a3bf8d91dd14b7462f8aa98f1d2740b609589a", size = 10171070, upload-time = "2025-12-04T09:01:44.917Z" }, - { url = "https://files.pythonhosted.org/packages/78/fa/4d8adeb9ff7fd32efcb9ebb05d5f61cd9ad4b4030390c76cd771fb38ac33/ty-0.0.1a31-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5fbc75e8e848929155b7ba0e9a222b2561222a1135bb492a9c5b9ad945c80b18", size = 10188190, upload-time = "2025-12-04T09:01:42.577Z" }, - { url = "https://files.pythonhosted.org/packages/f6/40/295903716cc2e4fdb88d8cf8b974f0936e6c021f35d5a7f78b769c746bcc/ty-0.0.1a31-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:915c0639dfb73f5f19cd69bbe89036ef18b8066ba88ce38d3e3cc0f39b32f99a", size = 9713419, upload-time = "2025-12-04T09:01:26.633Z" }, - { url = "https://files.pythonhosted.org/packages/f0/93/b622782ce78f0cbacf167c617b41f45e76de02e3d5d5898fc78ad7a47de7/ty-0.0.1a31-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:28d23f4e58e9b08fc8966ad8dac754b4cd5ccafed711e2a32a62f3d2cb6f44cb", size = 9170660, upload-time = "2025-12-04T09:02:01.556Z" }, - { url = "https://files.pythonhosted.org/packages/d9/5d/2a04dfd412c87d1da220260a5cf8444d36fa356d1f993ee1db5ad820df93/ty-0.0.1a31-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:cb2ebbc8065f4dd09e4b82f492becc55cad39068e842f82bfa1c9f7b9864b034", size = 9443773, upload-time = "2025-12-04T09:01:39.958Z" }, - { url = "https://files.pythonhosted.org/packages/1f/5e/18c0123b8dcd6a7e7f4a35d3eed127c6c0140377f5986df6bd01c9df5eb1/ty-0.0.1a31-py3-none-musllinux_1_2_i686.whl", hash = "sha256:966984a8a0e4f99d133e9b73bc778d4861b58467bdb85950805d67ff90e73e3e", size = 9532255, upload-time = "2025-12-04T09:01:29.243Z" }, - { url = "https://files.pythonhosted.org/packages/8a/c0/2570e4f891f33c3f9160f052d3759e9c7a3dee29bac5b93ad1f29ed42526/ty-0.0.1a31-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:bc4fe23fddaa78c0b11637a1eff152b95988960e5d240d282b41f827d13b28f0", size = 9837753, upload-time = "2025-12-04T09:02:04.361Z" }, - { url = "https://files.pythonhosted.org/packages/58/ad/6a231c11b95d3aa3f54824edfb7ab13ae13eea405bbcc6c80090551bd1b2/ty-0.0.1a31-py3-none-win32.whl", hash = "sha256:f82f4e051c40033ca9f10cffafc346fd86ea6541e786c2b1fcffa08c661efbaa", size = 9011568, upload-time = "2025-12-04T09:01:54.033Z" }, - { url = "https://files.pythonhosted.org/packages/f7/8b/c07438de84e3e9cbecedd2e8895dc25ca1b110847dd95b5e9f50124eb8d5/ty-0.0.1a31-py3-none-win_amd64.whl", hash = "sha256:12fae6138c4cbd143fe4b5c616056535353a2d0821062d8750132d3ea022fa8f", size = 9880831, upload-time = "2025-12-04T09:01:18.284Z" }, - { url = "https://files.pythonhosted.org/packages/a3/9c/ad589282e76e185eb54c3ce212182f7a28547ed20a5a08b51f9684dc2849/ty-0.0.1a31-py3-none-win_arm64.whl", hash = "sha256:4cc339de4dd4b8dd7167cfd1f826a25e303b3dec27da74596a0ce3ed83bcd293", size = 9380327, upload-time = "2025-12-04T09:01:49.651Z" }, + { url = "https://files.pythonhosted.org/packages/7c/98/c1f61ba378b4191e641bb36c07b7fcc70ff844d61be7a4bf2fea7472b4a9/ty-0.0.5-py3-none-linux_armv6l.whl", hash = "sha256:1594cd9bb68015eb2f5a3c68a040860f3c9306dc6667d7a0e5f4df9967b460e2", size = 9785554, upload-time = "2025-12-20T21:19:05.024Z" }, + { url = "https://files.pythonhosted.org/packages/ab/f9/b37b77c03396bd779c1397dae4279b7ad79315e005b3412feed8812a4256/ty-0.0.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:7c0140ba980233d28699d9ddfe8f43d0b3535d6a3bbff9935df625a78332a3cf", size = 9603995, upload-time = "2025-12-20T21:19:15.256Z" }, + { url = "https://files.pythonhosted.org/packages/7d/70/4e75c11903b0e986c0203040472627cb61d6a709e1797fb08cdf9d565743/ty-0.0.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:15de414712cde92048ae4b1a77c4dc22920bd23653fe42acaf73028bad88f6b9", size = 9145815, upload-time = "2025-12-20T21:19:36.481Z" }, + { url = "https://files.pythonhosted.org/packages/89/05/93983dfcf871a41dfe58e5511d28e6aa332a1f826cc67333f77ae41a2f8a/ty-0.0.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:438aa51ad6c5fae64191f8d58876266e26f9250cf09f6624b6af47a22fa88618", size = 9619849, upload-time = "2025-12-20T21:19:19.084Z" }, + { url = "https://files.pythonhosted.org/packages/82/b6/896ab3aad59f846823f202e94be6016fb3f72434d999d2ae9bd0f28b3af9/ty-0.0.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1b3d373fd96af1564380caf153600481c676f5002ee76ba8a7c3508cdff82ee0", size = 9606611, upload-time = "2025-12-20T21:19:24.583Z" }, + { url = "https://files.pythonhosted.org/packages/ca/ae/098e33fc92330285ed843e2750127e896140c4ebd2d73df7732ea496f588/ty-0.0.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8453692503212ad316cf8b99efbe85a91e5f63769c43be5345e435a1b16cba5a", size = 10029523, upload-time = "2025-12-20T21:19:07.055Z" }, + { url = "https://files.pythonhosted.org/packages/04/5a/f4b4c33758b9295e9aca0de9645deca0f4addd21d38847228723a6e780fc/ty-0.0.5-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:2e4c454139473abbd529767b0df7a795ed828f780aef8d0d4b144558c0dc4446", size = 10870892, upload-time = "2025-12-20T21:19:34.495Z" }, + { url = "https://files.pythonhosted.org/packages/c3/c5/4e3e7e88389365aa1e631c99378711cf0c9d35a67478cb4720584314cf44/ty-0.0.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:426d4f3b82475b1ec75f3cc9ee5a667c8a4ae8441a09fcd8e823a53b706d00c7", size = 10599291, upload-time = "2025-12-20T21:19:26.557Z" }, + { url = "https://files.pythonhosted.org/packages/c1/5d/138f859ea87bd95e17b9818e386ae25a910e46521c41d516bf230ed83ffc/ty-0.0.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5710817b67c6b2e4c0224e4f319b7decdff550886e9020f6d46aa1ce8f89a609", size = 10413515, upload-time = "2025-12-20T21:19:11.094Z" }, + { url = "https://files.pythonhosted.org/packages/27/21/1cbcd0d3b1182172f099e88218137943e0970603492fb10c7c9342369d9a/ty-0.0.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e23c55ef08882c7c5ced1ccb90b4eeefa97f690aea254f58ac0987896c590f76", size = 10144992, upload-time = "2025-12-20T21:19:13.225Z" }, + { url = "https://files.pythonhosted.org/packages/ad/30/fdac06a5470c09ad2659a0806497b71f338b395d59e92611f71b623d05a0/ty-0.0.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:b9e4c1a28a23b14cf8f4f793f4da396939f16c30bfa7323477c8cc234e352ac4", size = 9606408, upload-time = "2025-12-20T21:19:09.212Z" }, + { url = "https://files.pythonhosted.org/packages/09/93/e99dcd7f53295192d03efd9cbcec089a916f49cad4935c0160ea9adbd53d/ty-0.0.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:4e9ebb61529b9745af662e37c37a01ad743cdd2c95f0d1421705672874d806cd", size = 9630040, upload-time = "2025-12-20T21:19:38.165Z" }, + { url = "https://files.pythonhosted.org/packages/d7/f8/6d1e87186e4c35eb64f28000c1df8fd5f73167ce126c5e3dd21fd1204a23/ty-0.0.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:5eb191a8e332f50f56dfe45391bdd7d43dd4ef6e60884710fd7ce84c5d8c1eb5", size = 9754016, upload-time = "2025-12-20T21:19:32.79Z" }, + { url = "https://files.pythonhosted.org/packages/28/e6/20f989342cb3115852dda404f1d89a10a3ce93f14f42b23f095a3d1a00c9/ty-0.0.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:92ed7451a1e82ee134a2c24ca43b74dd31e946dff2b08e5c34473e6b051de542", size = 10252877, upload-time = "2025-12-20T21:19:20.787Z" }, + { url = "https://files.pythonhosted.org/packages/57/9d/fc66fa557443233dfad9ae197ff3deb70ae0efcfb71d11b30ef62f5cdcc3/ty-0.0.5-py3-none-win32.whl", hash = "sha256:71f6707e4c1c010c158029a688a498220f28bb22fdb6707e5c20e09f11a5e4f2", size = 9212640, upload-time = "2025-12-20T21:19:30.817Z" }, + { url = "https://files.pythonhosted.org/packages/68/b6/05c35f6dea29122e54af0e9f8dfedd0a100c721affc8cc801ebe2bc2ed13/ty-0.0.5-py3-none-win_amd64.whl", hash = "sha256:2b8b754a0d7191e94acdf0c322747fec34371a4d0669f5b4e89549aef28814ae", size = 10034701, upload-time = "2025-12-20T21:19:28.311Z" }, + { url = "https://files.pythonhosted.org/packages/df/ca/4201ed5cb2af73912663d0c6ded927c28c28b3c921c9348aa8d2cfef4853/ty-0.0.5-py3-none-win_arm64.whl", hash = "sha256:83bea5a5296caac20d52b790ded2b830a7ff91c4ed9f36730fe1f393ceed6654", size = 9566474, upload-time = "2025-12-20T21:19:22.518Z" }, ] [[package]]