Merge pull request #3383 from PrefectHQ/main

Update published docs
This commit is contained in:
Jeremiah Lowin 2026-03-03 19:07:34 -05:00 committed by GitHub
commit ec7a83f5f9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
38 changed files with 664 additions and 215 deletions

View file

@ -117,14 +117,17 @@ jobs:
**Branch**: {{ref}}
**Event**: {{eventName}}
### Common causes
- **ty (type checker)**: New ty releases frequently add stricter checks that flag previously-accepted code. Run `uv run ty check` locally with the latest ty to reproduce. Fix the type errors or bump the ty version floor in `pyproject.toml`.
- **ruff**: New lint rules or stricter defaults in a ruff upgrade.
- **mcp SDK**: Breaking changes in the `mcp` package (new method signatures, renamed types).
### What to do
This likely means that upgraded dependencies have introduced new errors. Please review the failed jobs and determine if the code needs to be updated or if dependency constraints need to be adjusted.
- [ ] Review the failure in the workflow run
- [ ] Identify root cause (dependency changes, new linter/type rules, etc.)
- [ ] Implement fix or adjust dependency constraints
- [ ] Verify fix resolves the issue
1. Check the workflow logs to identify which job failed (static analysis vs tests)
2. Reproduce locally with `uv sync --upgrade && uv run prek run --all-files && uv run pytest -n auto`
3. Fix the code or adjust dependency constraints as needed
---
*This issue was automatically created by a GitHub Action.*

View file

@ -163,7 +163,6 @@
"collapsed": true,
"group": "Authentication",
"icon": "key",
"tag": "UPDATED",
"pages": [
"servers/auth/authentication",
"servers/auth/token-verification",
@ -172,7 +171,8 @@
"servers/auth/oidc-proxy",
"servers/auth/full-oauth-server",
"servers/auth/multi-auth"
]
],
"tag": "UPDATED"
},
"servers/authorization",
{
@ -216,7 +216,6 @@
"collapsed": true,
"group": "Handlers",
"icon": "hand",
"tag": "UPDATED",
"pages": [
"clients/notifications",
"clients/sampling",
@ -225,7 +224,8 @@
"clients/progress",
"clients/logging",
"clients/roots"
]
],
"tag": "UPDATED"
},
{
"collapsed": true,
@ -428,6 +428,7 @@
"pages": [
"python-sdk/fastmcp-client-sampling-handlers-__init__",
"python-sdk/fastmcp-client-sampling-handlers-anthropic",
"python-sdk/fastmcp-client-sampling-handlers-google_genai",
"python-sdk/fastmcp-client-sampling-handlers-openai"
]
}

View file

@ -10,7 +10,7 @@ Google GenAI sampling handler with tool support for FastMCP 3.0.
## Classes
### `GoogleGenaiSamplingHandler` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/sampling/handlers/google_genai.py#L54" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `GoogleGenaiSamplingHandler` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/client/sampling/handlers/google_genai.py#L54" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
Sampling handler that uses the Google GenAI API with tool support.

View file

@ -60,9 +60,11 @@ Defaults to BM25 ranking.
``"brief"`` returns tool names and descriptions only.
``"detailed"`` returns compact markdown with parameter schemas.
``"full"`` returns complete JSON tool definitions.
- `default_limit`: Maximum number of results to return.
The LLM can override this per call. ``None`` means no limit.
### `GetSchemas` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/experimental/transforms/code_mode.py#L245" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `GetSchemas` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/experimental/transforms/code_mode.py#L262" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
Discovery tool factory that returns schemas for tools by name.
@ -76,7 +78,7 @@ types, and required markers.
``"full"`` returns the complete JSON schema.
### `GetTags` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/experimental/transforms/code_mode.py#L306" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `GetTags` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/experimental/transforms/code_mode.py#L323" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
Discovery tool factory that lists tool tags from the catalog.
@ -91,7 +93,20 @@ without tags appear under ``"untagged"``.
``"full"`` lists all tools under each tag.
### `CodeMode` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/experimental/transforms/code_mode.py#L382" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `ListTools` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/experimental/transforms/code_mode.py#L390" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
Discovery tool factory that lists all tools in the catalog.
**Args:**
- `name`: Name of the synthetic tool exposed to the LLM.
- `default_detail`: Default detail level.
``"brief"`` returns tool names and one-line descriptions.
``"detailed"`` returns compact markdown with parameter schemas.
``"full"`` returns the complete JSON schema.
### `CodeMode` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/experimental/transforms/code_mode.py#L439" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
Transform that collapses all tools into discovery + execute meta-tools.
@ -108,13 +123,13 @@ environment with ``call_tool(name, params)`` in scope.
**Methods:**
#### `transform_tools` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/experimental/transforms/code_mode.py#L432" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `transform_tools` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/experimental/transforms/code_mode.py#L489" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
transform_tools(self, tools: Sequence[Tool]) -> Sequence[Tool]
```
#### `get_tool` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/experimental/transforms/code_mode.py#L435" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `get_tool` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/experimental/transforms/code_mode.py#L492" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
get_tool(self, name: str, call_next: GetToolNext) -> Tool | None

View file

@ -13,7 +13,7 @@ This module contains HTML generation functions for consent and error pages.
## Functions
### `create_consent_html` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/oauth_proxy/ui.py#L22" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `create_consent_html` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/oauth_proxy/ui.py#L20" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
create_consent_html(client_id: str, redirect_uri: str, scopes: list[str], txn_id: str, csrf_token: str, client_name: str | None = None, title: str = 'Application Access Request', server_name: str | None = None, server_icon_url: str | None = None, server_website_url: str | None = None, client_website_url: str | None = None, csp_policy: str | None = None, is_cimd_client: bool = False, cimd_domain: str | None = None) -> str
@ -29,7 +29,7 @@ If empty string "", disables CSP entirely (no meta tag is rendered).
If a non-empty string, uses that as the CSP policy value.
### `create_error_html` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/oauth_proxy/ui.py#L224" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `create_error_html` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/oauth_proxy/ui.py#L215" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
create_error_html(error_title: str, error_message: str, error_details: dict[str, str] | None = None, server_name: str | None = None, server_icon_url: str | None = None) -> str

View file

@ -66,7 +66,7 @@ Features:
**Methods:**
#### `get_token_verifier` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/providers/aws.py#L172" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `get_token_verifier` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/providers/aws.py#L174" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
get_token_verifier(self) -> TokenVerifier

View file

@ -14,7 +14,7 @@ using the OAuth Proxy pattern for non-DCR OAuth flows.
## Functions
### `EntraOBOToken` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/providers/azure.py#L681" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `EntraOBOToken` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/providers/azure.py#L683" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
EntraOBOToken(scopes: list[str]) -> str
@ -78,7 +78,7 @@ Setup:
**Methods:**
#### `authorize` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/providers/azure.py#L250" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `authorize` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/providers/azure.py#L252" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
authorize(self, client: OAuthClientInformationFull, params: AuthorizationParams) -> str
@ -98,7 +98,7 @@ scopes to determine the resource/audience instead of a separate parameter.
- Authorization URL to redirect the user to Azure AD
#### `get_obo_credential` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/providers/azure.py#L476" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `get_obo_credential` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/providers/azure.py#L478" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
get_obo_credential(self, user_assertion: str) -> OnBehalfOfCredential
@ -120,7 +120,7 @@ calls multiple tools with the same scopes.
- `ImportError`: If azure-identity is not installed (requires fastmcp[azure]).
#### `close_obo_credentials` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/providers/azure.py#L517" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `close_obo_credentials` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/providers/azure.py#L519" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
close_obo_credentials(self) -> None
@ -129,7 +129,7 @@ close_obo_credentials(self) -> None
Close all cached OBO credentials.
### `AzureJWTVerifier` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/providers/azure.py#L528" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `AzureJWTVerifier` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/providers/azure.py#L530" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
JWT verifier pre-configured for Azure AD / Microsoft Entra ID.
@ -166,7 +166,7 @@ Example::
**Methods:**
#### `scopes_supported` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/providers/azure.py#L608" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `scopes_supported` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/providers/azure.py#L610" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
scopes_supported(self) -> list[str]

View file

@ -59,7 +59,7 @@ Setup Requirements:
4. Note your Client ID and Client Secret
### `AuthKitProvider` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/providers/workos.py#L230" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `AuthKitProvider` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/providers/workos.py#L232" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
AuthKit metadata provider for DCR (Dynamic Client Registration).
@ -85,7 +85,7 @@ https://workos.com/docs/authkit/mcp/integrating/token-verification
**Methods:**
#### `get_routes` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/providers/workos.py#L317" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `get_routes` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/providers/workos.py#L319" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
get_routes(self, mcp_path: str | None = None) -> list[Route]

View file

@ -18,7 +18,7 @@ executed.
## Classes
### `FastMCPProviderTool` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/fastmcp_provider.py#L54" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `FastMCPProviderTool` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/fastmcp_provider.py#L72" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
Tool that delegates execution to a wrapped server's middleware.
@ -30,7 +30,7 @@ chain is executed.
**Methods:**
#### `wrap` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/fastmcp_provider.py#L76" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `wrap` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/fastmcp_provider.py#L94" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
wrap(cls, server: Any, tool: Tool) -> FastMCPProviderTool
@ -39,7 +39,7 @@ wrap(cls, server: Any, tool: Tool) -> FastMCPProviderTool
Wrap a Tool to delegate execution to the server's middleware.
#### `run` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/fastmcp_provider.py#L129" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `run` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/fastmcp_provider.py#L147" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
run(self, arguments: dict[str, Any]) -> ToolResult
@ -51,13 +51,13 @@ This is called when the tool is used within a TransformedTool
forwarding function or other contexts where task_meta is not available.
#### `get_span_attributes` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/fastmcp_provider.py#L148" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `get_span_attributes` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/fastmcp_provider.py#L166" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
get_span_attributes(self) -> dict[str, Any]
```
### `FastMCPProviderResource` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/fastmcp_provider.py#L155" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `FastMCPProviderResource` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/fastmcp_provider.py#L173" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
Resource that delegates reading to a wrapped server's read_resource().
@ -68,7 +68,7 @@ When `read()` is called, this resource invokes the wrapped server's
**Methods:**
#### `wrap` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/fastmcp_provider.py#L176" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `wrap` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/fastmcp_provider.py#L194" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
wrap(cls, server: Any, resource: Resource) -> FastMCPProviderResource
@ -77,13 +77,13 @@ wrap(cls, server: Any, resource: Resource) -> FastMCPProviderResource
Wrap a Resource to delegate reading to the server's middleware.
#### `get_span_attributes` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/fastmcp_provider.py#L219" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `get_span_attributes` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/fastmcp_provider.py#L237" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
get_span_attributes(self) -> dict[str, Any]
```
### `FastMCPProviderPrompt` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/fastmcp_provider.py#L226" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `FastMCPProviderPrompt` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/fastmcp_provider.py#L244" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
Prompt that delegates rendering to a wrapped server's render_prompt().
@ -94,7 +94,7 @@ When `render()` is called, this prompt invokes the wrapped server's
**Methods:**
#### `wrap` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/fastmcp_provider.py#L247" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `wrap` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/fastmcp_provider.py#L265" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
wrap(cls, server: Any, prompt: Prompt) -> FastMCPProviderPrompt
@ -103,7 +103,7 @@ wrap(cls, server: Any, prompt: Prompt) -> FastMCPProviderPrompt
Wrap a Prompt to delegate rendering to the server's middleware.
#### `render` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/fastmcp_provider.py#L298" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `render` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/fastmcp_provider.py#L316" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
render(self, arguments: dict[str, Any] | None = None) -> PromptResult
@ -115,13 +115,13 @@ This is called when the prompt is used within a transformed context
or other contexts where task_meta is not available.
#### `get_span_attributes` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/fastmcp_provider.py#L317" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `get_span_attributes` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/fastmcp_provider.py#L335" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
get_span_attributes(self) -> dict[str, Any]
```
### `FastMCPProviderResourceTemplate` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/fastmcp_provider.py#L324" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `FastMCPProviderResourceTemplate` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/fastmcp_provider.py#L342" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
Resource template that creates FastMCPProviderResources.
@ -133,7 +133,7 @@ when read.
**Methods:**
#### `wrap` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/fastmcp_provider.py#L346" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `wrap` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/fastmcp_provider.py#L364" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
wrap(cls, server: Any, template: ResourceTemplate) -> FastMCPProviderResourceTemplate
@ -142,7 +142,7 @@ wrap(cls, server: Any, template: ResourceTemplate) -> FastMCPProviderResourceTem
Wrap a ResourceTemplate to create FastMCPProviderResources.
#### `create_resource` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/fastmcp_provider.py#L367" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `create_resource` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/fastmcp_provider.py#L385" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
create_resource(self, uri: str, params: dict[str, Any]) -> Resource
@ -155,7 +155,7 @@ We use `_original_uri_template` with `params` to construct the internal
URI that the nested server understands.
#### `read` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/fastmcp_provider.py#L417" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `read` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/fastmcp_provider.py#L435" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
read(self, arguments: dict[str, Any]) -> str | bytes | ResourceResult
@ -167,7 +167,7 @@ Reads the resource via the wrapped server and returns the ResourceResult.
This method is called by Docket during background task execution.
#### `register_with_docket` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/fastmcp_provider.py#L438" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `register_with_docket` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/fastmcp_provider.py#L456" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
register_with_docket(self, docket: Docket) -> None
@ -176,7 +176,7 @@ register_with_docket(self, docket: Docket) -> None
No-op: the child's actual template is registered via get_tasks().
#### `add_to_docket` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/fastmcp_provider.py#L441" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `add_to_docket` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/fastmcp_provider.py#L459" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
add_to_docket(self, docket: Docket, params: dict[str, Any], **kwargs: Any) -> Execution
@ -188,13 +188,13 @@ The child's FunctionResourceTemplate.fn is registered (via get_tasks),
and it expects splatted **kwargs, so we splat params here.
#### `get_span_attributes` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/fastmcp_provider.py#L460" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `get_span_attributes` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/fastmcp_provider.py#L478" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
get_span_attributes(self) -> dict[str, Any]
```
### `FastMCPProvider` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/fastmcp_provider.py#L472" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `FastMCPProvider` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/fastmcp_provider.py#L490" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
Provider that wraps a FastMCP server.
@ -210,7 +210,7 @@ This ensures middleware runs when components are executed.
**Methods:**
#### `get_tasks` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/fastmcp_provider.py#L637" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `get_tasks` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/fastmcp_provider.py#L655" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
get_tasks(self) -> Sequence[FastMCPComponent]
@ -224,7 +224,7 @@ server's transforms applied, then applies this provider's transforms
for correct registration keys.
#### `lifespan` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/fastmcp_provider.py#L678" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `lifespan` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/fastmcp_provider.py#L696" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
lifespan(self) -> AsyncIterator[None]

View file

@ -15,16 +15,18 @@ Version filter transform for filtering components by version range.
Filters components by version range.
When applied to a provider or server, only components within the version
range are visible. Within that filtered set, the highest version of each
component is exposed to clients (standard deduplication behavior).
When applied to a provider or server, components within the version range
are visible, and unversioned components are included by default. Within
that filtered set, the highest version of each component is exposed to
clients (standard deduplication behavior). Set
``include_unversioned=False`` to exclude unversioned components.
Parameters mirror comparison operators for clarity:
# Versions &lt; 3.0 (v1 and v2)
# Versions < 3.0 (v1 and v2)
server.add_transform(VersionFilter(version_lt="3.0"))
# Versions &gt;= 2.0 and < 3.0 (only v2.x)
# Versions >= 2.0 and < 3.0 (only v2.x)
server.add_transform(VersionFilter(version_gte="2.0", version_lt="3.0"))
Works with any version string - PEP 440 (1.0, 2.0) or dates (2025-01-01).
@ -32,53 +34,55 @@ Works with any version string - PEP 440 (1.0, 2.0) or dates (2025-01-01).
**Args:**
- `version_gte`: Versions >= this value pass through.
- `version_lt`: Versions < this value pass through.
- `include_unversioned`: Whether unversioned components (``version=None``)
should pass through the filter. Defaults to True.
**Methods:**
#### `list_tools` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/transforms/version_filter.py#L72" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `list_tools` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/transforms/version_filter.py#L80" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
list_tools(self, tools: Sequence[Tool]) -> Sequence[Tool]
```
#### `get_tool` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/transforms/version_filter.py#L75" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `get_tool` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/transforms/version_filter.py#L87" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
get_tool(self, name: str, call_next: GetToolNext) -> Tool | None
```
#### `list_resources` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/transforms/version_filter.py#L84" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `list_resources` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/transforms/version_filter.py#L96" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
list_resources(self, resources: Sequence[Resource]) -> Sequence[Resource]
```
#### `get_resource` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/transforms/version_filter.py#L87" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `get_resource` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/transforms/version_filter.py#L103" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
get_resource(self, uri: str, call_next: GetResourceNext) -> Resource | None
```
#### `list_resource_templates` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/transforms/version_filter.py#L100" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `list_resource_templates` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/transforms/version_filter.py#L116" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
list_resource_templates(self, templates: Sequence[ResourceTemplate]) -> Sequence[ResourceTemplate]
```
#### `get_resource_template` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/transforms/version_filter.py#L105" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `get_resource_template` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/transforms/version_filter.py#L125" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
get_resource_template(self, uri: str, call_next: GetResourceTemplateNext) -> ResourceTemplate | None
```
#### `list_prompts` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/transforms/version_filter.py#L118" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `list_prompts` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/transforms/version_filter.py#L138" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
list_prompts(self, prompts: Sequence[Prompt]) -> Sequence[Prompt]
```
#### `get_prompt` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/transforms/version_filter.py#L121" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `get_prompt` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/transforms/version_filter.py#L145" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
get_prompt(self, name: str, call_next: GetPromptNext) -> Prompt | None

68
docs/unify-intent.js Normal file
View file

@ -0,0 +1,68 @@
// Load Unify intent tag on authentication pages only
(function () {
if (typeof window === "undefined") return;
function isAuthPage() {
var path = window.location.pathname;
return path.includes("/servers/auth/") || path.includes("/clients/auth/");
}
function loadUnify() {
var e = [
"identify",
"page",
"startAutoPage",
"stopAutoPage",
"startAutoIdentify",
"stopAutoIdentify",
];
function t(o) {
return Object.assign(
[],
e.reduce(function (r, n) {
r[n] = function () {
return o.push([n, [].slice.call(arguments)]), o;
};
return r;
}, {}),
);
}
if (!window.unify) window.unify = t(window.unify);
if (!window.unifyBrowser) window.unifyBrowser = t(window.unifyBrowser);
var n = document.createElement("script");
n.async = true;
n.setAttribute(
"src",
"https://tag.unifyintent.com/v1/Rj9KrQqMhyYcU5qfJtVszE/script.js",
);
n.setAttribute(
"data-api-key",
"wk_SBvJ4jyD_wRgPAHCNJb89seVmREhcj2NspRpxAywi",
);
n.setAttribute("id", "unifytag");
(document.body || document.head).appendChild(n);
}
function update() {
if (isAuthPage() && !document.getElementById("unifytag")) {
loadUnify();
} else if (!isAuthPage() && document.getElementById("unifytag")) {
document.getElementById("unifytag").remove();
}
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", update);
} else {
update();
}
var lastUrl = location.href;
new MutationObserver(function () {
if (location.href !== lastUrl) {
lastUrl = location.href;
setTimeout(update, 100);
}
}).observe(document.body, { subtree: true, childList: true });
})();

View file

@ -84,7 +84,7 @@ dev = [
"pytest-timeout>=2.4.0",
"pytest-xdist>=3.6.1",
"ruff>=0.12.8",
"ty>=0.0.15",
"ty>=0.0.20",
"prek>=0.2.12",
"loq>=0.1.0a3",
"opentelemetry-exporter-otlp-proto-grpc>=1.39.0",

View file

@ -1,6 +1,6 @@
import inspect
from collections.abc import Awaitable, Callable
from typing import TypeAlias
from typing import TypeAlias, cast
import mcp.types
import pydantic
@ -66,7 +66,9 @@ def _create_roots_callback_from_fn(
roots = fn(context)
if inspect.isawaitable(roots):
roots = await roots
return mcp.types.ListRootsResult(roots=convert_roots_list(roots))
return mcp.types.ListRootsResult(
roots=convert_roots_list(cast(RootsList, roots))
)
except Exception as e:
return mcp.types.ErrorData(
code=mcp.types.INTERNAL_ERROR,

View file

@ -53,6 +53,8 @@ def create_sampling_callback(
if inspect.isawaitable(result):
result = await result
result = cast(SamplingHandlerResult, result)
if isinstance(result, str):
result = CreateMessageResult(
role="assistant",

View file

@ -541,16 +541,9 @@ class CodeMode(CatalogTransform):
ctx: Context = None, # type: ignore[assignment]
) -> Any:
"""Execute tool calls using Python code."""
cached_tools: Sequence[Tool] | None = None
async def _get_cached_tools() -> Sequence[Tool]:
nonlocal cached_tools
if cached_tools is None:
cached_tools = await transform.get_tool_catalog(ctx)
return cached_tools
async def call_tool(tool_name: str, params: dict[str, Any]) -> Any:
backend_tools = await _get_cached_tools()
backend_tools = await transform.get_tool_catalog(ctx)
tool = transform._find_tool(tool_name, backend_tools)
if tool is None:
raise NotFoundError(f"Unknown tool: {tool_name}")

View file

@ -5,8 +5,6 @@ This module contains HTML generation functions for consent and error pages.
from __future__ import annotations
from urllib.parse import urlparse
from fastmcp.utilities.ui import (
BUTTON_STYLES,
DETAIL_BOX_STYLES,
@ -198,20 +196,13 @@ def create_consent_html(
# If csp_policy is empty string, CSP will be disabled entirely in create_page
# If csp_policy is a non-empty string, use it as-is
if csp_policy is None:
# Need to allow form-action for form submission
# Chrome requires explicit scheme declarations in CSP form-action when redirect chains
# end in custom protocol schemes (e.g., cursor://). Parse redirect_uri to include its scheme.
parsed_redirect = urlparse(redirect_uri)
redirect_scheme = parsed_redirect.scheme.lower()
# Build form-action directive with standard schemes plus custom protocol if present
form_action_schemes = ["https:", "http:"]
if redirect_scheme and redirect_scheme not in ("http", "https"):
# Custom protocol scheme (e.g., cursor:, vscode:, etc.)
form_action_schemes.append(f"{redirect_scheme}:")
form_action_directive = " ".join(form_action_schemes)
csp_policy = f"default-src 'none'; style-src 'unsafe-inline'; img-src https: data:; base-uri 'none'; form-action {form_action_directive}"
# The consent form posts to itself (action="") and all subsequent redirects
# are server-controlled. Chrome enforces form-action across the entire redirect
# chain (Chromium issue #40923007), which breaks flows where an HTTPS callback
# internally redirects to a custom scheme (e.g., claude:// or cursor://).
# Since the form target is same-origin and we control the redirect chain,
# omitting form-action is safe and avoids these browser-specific CSP issues.
csp_policy = "default-src 'none'; style-src 'unsafe-inline'; img-src https: data:; base-uri 'none'"
return create_page(
content=content,

View file

@ -70,6 +70,7 @@ class Auth0Provider(OIDCProxy):
client_storage: AsyncKeyValue | None = None,
jwt_signing_key: str | bytes | None = None,
require_authorization_consent: bool = True,
consent_csp_policy: str | None = None,
) -> None:
"""Initialize Auth0 OAuth provider.
@ -114,6 +115,7 @@ class Auth0Provider(OIDCProxy):
client_storage=client_storage,
jwt_signing_key=jwt_signing_key,
require_authorization_consent=require_authorization_consent,
consent_csp_policy=consent_csp_policy,
)
logger.debug(

View file

@ -109,6 +109,7 @@ class AWSCognitoProvider(OIDCProxy):
client_storage: AsyncKeyValue | None = None,
jwt_signing_key: str | bytes | None = None,
require_authorization_consent: bool = True,
consent_csp_policy: str | None = None,
):
"""Initialize AWS Cognito OAuth provider.
@ -161,6 +162,7 @@ class AWSCognitoProvider(OIDCProxy):
client_storage=client_storage,
jwt_signing_key=jwt_signing_key,
require_authorization_consent=require_authorization_consent,
consent_csp_policy=consent_csp_policy,
)
logger.debug(

View file

@ -108,6 +108,7 @@ class AzureProvider(OAuthProxy):
client_storage: AsyncKeyValue | None = None,
jwt_signing_key: str | bytes | None = None,
require_authorization_consent: bool = True,
consent_csp_policy: str | None = None,
base_authority: str = "login.microsoftonline.com",
http_client: httpx.AsyncClient | None = None,
) -> None:
@ -233,6 +234,7 @@ class AzureProvider(OAuthProxy):
client_storage=client_storage,
jwt_signing_key=jwt_signing_key,
require_authorization_consent=require_authorization_consent,
consent_csp_policy=consent_csp_policy,
valid_scopes=parsed_required_scopes,
)

View file

@ -193,6 +193,7 @@ class DiscordProvider(OAuthProxy):
client_storage: AsyncKeyValue | None = None,
jwt_signing_key: str | bytes | None = None,
require_authorization_consent: bool = True,
consent_csp_policy: str | None = None,
http_client: httpx.AsyncClient | None = None,
):
"""Initialize Discord OAuth provider.
@ -253,6 +254,7 @@ class DiscordProvider(OAuthProxy):
client_storage=client_storage,
jwt_signing_key=jwt_signing_key,
require_authorization_consent=require_authorization_consent,
consent_csp_policy=consent_csp_policy,
)
logger.debug(

View file

@ -192,6 +192,7 @@ class GitHubProvider(OAuthProxy):
client_storage: AsyncKeyValue | None = None,
jwt_signing_key: str | bytes | None = None,
require_authorization_consent: bool = True,
consent_csp_policy: str | None = None,
http_client: httpx.AsyncClient | None = None,
):
"""Initialize GitHub OAuth provider.
@ -247,6 +248,7 @@ class GitHubProvider(OAuthProxy):
client_storage=client_storage,
jwt_signing_key=jwt_signing_key,
require_authorization_consent=require_authorization_consent,
consent_csp_policy=consent_csp_policy,
)
logger.debug(

View file

@ -207,6 +207,7 @@ class GoogleProvider(OAuthProxy):
client_storage: AsyncKeyValue | None = None,
jwt_signing_key: str | bytes | None = None,
require_authorization_consent: bool = True,
consent_csp_policy: str | None = None,
extra_authorize_params: dict[str, str] | None = None,
http_client: httpx.AsyncClient | None = None,
):
@ -283,6 +284,7 @@ class GoogleProvider(OAuthProxy):
client_storage=client_storage,
jwt_signing_key=jwt_signing_key,
require_authorization_consent=require_authorization_consent,
consent_csp_policy=consent_csp_policy,
extra_authorize_params=extra_authorize_params_final,
)

View file

@ -129,6 +129,7 @@ class OCIProvider(OIDCProxy):
client_storage: AsyncKeyValue | None = None,
jwt_signing_key: str | bytes | None = None,
require_authorization_consent: bool = True,
consent_csp_policy: str | None = None,
) -> None:
"""Initialize OCI OIDC provider.
@ -161,6 +162,7 @@ class OCIProvider(OIDCProxy):
client_storage=client_storage,
jwt_signing_key=jwt_signing_key,
require_authorization_consent=require_authorization_consent,
consent_csp_policy=consent_csp_policy,
)
logger.debug(

View file

@ -157,6 +157,7 @@ class WorkOSProvider(OAuthProxy):
client_storage: AsyncKeyValue | None = None,
jwt_signing_key: str | bytes | None = None,
require_authorization_consent: bool = True,
consent_csp_policy: str | None = None,
http_client: httpx.AsyncClient | None = None,
):
"""Initialize WorkOS OAuth provider.
@ -218,6 +219,7 @@ class WorkOSProvider(OAuthProxy):
client_storage=client_storage,
jwt_signing_key=jwt_signing_key,
require_authorization_consent=require_authorization_consent,
consent_csp_policy=consent_csp_policy,
)
logger.debug(

View file

@ -17,6 +17,7 @@ from contextvars import ContextVar, Token
from dataclasses import dataclass
from datetime import datetime, timezone
from functools import lru_cache
from types import TracebackType
from typing import TYPE_CHECKING, Any, Protocol, cast, get_type_hints, runtime_checkable
from mcp.server.auth.middleware.auth_context import (
@ -851,14 +852,19 @@ class _CurrentContext(Dependency["Context"]):
"Check `context.request_context` for None before accessing."
)
async def __aexit__(self, *args: object) -> None:
async def __aexit__(
self,
exc_type: type[BaseException] | None,
exc_value: BaseException | None,
traceback: TracebackType | None,
) -> None:
# Clean up access token ContextVar
if self._access_token_cv_token is not None:
_task_access_token.reset(self._access_token_cv_token)
self._access_token_cv_token = None
# Clean up if we created a context for background task
if self._context is not None:
await self._context.__aexit__(*args)
await self._context.__aexit__(exc_type, exc_value, traceback)
self._context = None
@ -883,10 +889,15 @@ class _OptionalCurrentContext(Dependency["Context | None"]):
self._inner = inner
return context
async def __aexit__(self, *args: object) -> None:
async def __aexit__(
self,
exc_type: type[BaseException] | None,
exc_value: BaseException | None,
traceback: TracebackType | None,
) -> None:
if self._inner is None:
return
await self._inner.__aexit__(*args)
await self._inner.__aexit__(exc_type, exc_value, traceback)
self._inner = None
@ -934,7 +945,12 @@ class _CurrentDocket(Dependency["Docket"]):
)
return docket
async def __aexit__(self, *args: object) -> None:
async def __aexit__(
self,
exc_type: type[BaseException] | None,
exc_value: BaseException | None,
traceback: TracebackType | None,
) -> None:
pass
@ -979,7 +995,12 @@ class _CurrentWorker(Dependency["Worker"]):
)
return worker
async def __aexit__(self, *args: object) -> None:
async def __aexit__(
self,
exc_type: type[BaseException] | None,
exc_value: BaseException | None,
traceback: TracebackType | None,
) -> None:
pass
@ -1021,7 +1042,12 @@ class _CurrentFastMCP(Dependency["FastMCP"]):
raise RuntimeError("FastMCP server instance is no longer available")
return server
async def __aexit__(self, *args: object) -> None:
async def __aexit__(
self,
exc_type: type[BaseException] | None,
exc_value: BaseException | None,
traceback: TracebackType | None,
) -> None:
pass
@ -1056,7 +1082,12 @@ class _CurrentRequest(Dependency[Request]):
async def __aenter__(self) -> Request:
return get_http_request()
async def __aexit__(self, *args: object) -> None:
async def __aexit__(
self,
exc_type: type[BaseException] | None,
exc_value: BaseException | None,
traceback: TracebackType | None,
) -> None:
pass
@ -1092,7 +1123,12 @@ class _CurrentHeaders(Dependency[dict[str, str]]):
async def __aenter__(self) -> dict[str, str]:
return get_http_headers(include={"authorization"})
async def __aexit__(self, *args: object) -> None:
async def __aexit__(
self,
exc_type: type[BaseException] | None,
exc_value: BaseException | None,
traceback: TracebackType | None,
) -> None:
pass
@ -1175,7 +1211,12 @@ class InMemoryProgress:
async def __aenter__(self) -> InMemoryProgress:
return self
async def __aexit__(self, *args: object) -> None:
async def __aexit__(
self,
exc_type: type[BaseException] | None,
exc_value: BaseException | None,
traceback: TracebackType | None,
) -> None:
pass
@property
@ -1243,7 +1284,12 @@ class Progress(Dependency["Progress"]):
self._impl = InMemoryProgress()
return self
async def __aexit__(self, *args: object) -> None:
async def __aexit__(
self,
exc_type: type[BaseException] | None,
exc_value: BaseException | None,
traceback: TracebackType | None,
) -> None:
self._impl = None
@property
@ -1309,7 +1355,12 @@ class _CurrentAccessToken(Dependency[AccessToken]):
)
return token
async def __aexit__(self, *args: object) -> None:
async def __aexit__(
self,
exc_type: type[BaseException] | None,
exc_value: BaseException | None,
traceback: TracebackType | None,
) -> None:
if self._access_token_cv_token is not None:
_task_access_token.reset(self._access_token_cv_token)
self._access_token_cv_token = None
@ -1363,7 +1414,12 @@ class _TokenClaim(Dependency[str]):
)
return str(value)
async def __aexit__(self, *args: object) -> None:
async def __aexit__(
self,
exc_type: type[BaseException] | None,
exc_value: BaseException | None,
traceback: TracebackType | None,
) -> None:
pass

View file

@ -14,62 +14,16 @@ from fastmcp.exceptions import DisabledError, NotFoundError
from fastmcp.server.tasks.config import TaskMeta
from fastmcp.utilities.logging import get_logger
from fastmcp.utilities.pagination import paginate_sequence
from fastmcp.utilities.versions import VersionSpec, parse_version_key, version_sort_key
from fastmcp.utilities.versions import VersionSpec, dedupe_with_versions
if TYPE_CHECKING:
from fastmcp.server.server import FastMCP
logger = get_logger(__name__)
C = TypeVar("C", bound=Any)
PaginateT = TypeVar("PaginateT")
def _dedupe_with_versions(
components: Sequence[C],
key_fn: Callable[[C], str],
) -> list[C]:
"""Deduplicate components by key, keeping highest version.
Groups components by key, selects the highest version from each group,
and injects available versions into meta if any component is versioned.
Args:
components: Sequence of components to deduplicate.
key_fn: Function to extract the grouping key from a component.
Returns:
Deduplicated list with versions injected into meta.
"""
by_key: dict[str, list[C]] = {}
for c in components:
by_key.setdefault(key_fn(c), []).append(c)
result: list[C] = []
for versions in by_key.values():
highest: C = cast(C, max(versions, key=version_sort_key))
if any(c.version is not None for c in versions):
all_versions = sorted(
[c.version for c in versions if c.version is not None],
key=parse_version_key,
reverse=True,
)
meta = highest.meta or {}
highest = highest.model_copy(
update={
"meta": {
**meta,
"fastmcp": {
**meta.get("fastmcp", {}),
"versions": all_versions,
},
}
}
)
result.append(highest)
return result
def _apply_pagination(
items: Sequence[PaginateT],
cursor: str | None,
@ -152,7 +106,7 @@ class MCPOperationsMixin:
server = cast("FastMCP", self)
logger.debug(f"[{server.name}] Handler called: list_tools")
tools = _dedupe_with_versions(list(await server.list_tools()), lambda t: t.name)
tools = dedupe_with_versions(list(await server.list_tools()), lambda t: t.name)
sdk_tools = [tool.to_mcp_tool(name=tool.name) for tool in tools]
# SDK may pass None for internal cache refresh despite type hint
@ -172,7 +126,7 @@ class MCPOperationsMixin:
server = cast("FastMCP", self)
logger.debug(f"[{server.name}] Handler called: list_resources")
resources = _dedupe_with_versions(
resources = dedupe_with_versions(
list(await server.list_resources()), lambda r: str(r.uri)
)
sdk_resources = [
@ -195,7 +149,7 @@ class MCPOperationsMixin:
server = cast("FastMCP", self)
logger.debug(f"[{server.name}] Handler called: list_resource_templates")
templates = _dedupe_with_versions(
templates = dedupe_with_versions(
list(await server.list_resource_templates()), lambda t: t.uri_template
)
sdk_templates = [
@ -220,7 +174,7 @@ class MCPOperationsMixin:
server = cast("FastMCP", self)
logger.debug(f"[{server.name}] Handler called: list_prompts")
prompts = _dedupe_with_versions(
prompts = dedupe_with_versions(
list(await server.list_prompts()), lambda p: p.name
)
sdk_prompts = [prompt.to_mcp_prompt(name=prompt.name) for prompt in prompts]

View file

@ -14,6 +14,7 @@ import re
from collections.abc import AsyncIterator, Sequence
from contextlib import asynccontextmanager
from typing import TYPE_CHECKING, Any, overload
from urllib.parse import quote
import mcp.types
from mcp.types import AnyUrl
@ -38,11 +39,28 @@ if TYPE_CHECKING:
def _expand_uri_template(template: str, params: dict[str, Any]) -> str:
"""Expand a URI template with parameters.
Simple implementation that handles {name} style placeholders.
Handles both {name} path placeholders and RFC 6570 {?param1,param2}
query parameter syntax.
"""
result = template
# Replace {name} path placeholders
for key, value in params.items():
result = re.sub(rf"\{{{key}\}}", str(value), result)
# Expand {?param1,param2,...} query parameter blocks
def _expand_query_block(match: re.Match[str]) -> str:
names = [n.strip() for n in match.group(1).split(",")]
parts = []
for name in names:
if name in params:
parts.append(f"{quote(name)}={quote(str(params[name]))}")
if parts:
return "?" + "&".join(parts)
return ""
result = re.sub(r"\{\?([^}]+)\}", _expand_query_block, result)
return result

View file

@ -227,6 +227,8 @@ async def call_sampling_handler(
if inspect.isawaitable(result):
result = await result
result = cast("str | CreateMessageResult | CreateMessageResultWithTools", result)
# Convert string to CreateMessageResult
if isinstance(result, str):
return CreateMessageResult(

View file

@ -50,6 +50,7 @@ from contextvars import ContextVar
from typing import TYPE_CHECKING
from fastmcp.server.transforms import Transform
from fastmcp.utilities.versions import dedupe_with_versions
if TYPE_CHECKING:
from fastmcp.prompts.prompt import Prompt
@ -172,6 +173,10 @@ class CatalogTransform(Transform):
) -> Sequence[Tool]:
"""Fetch the real tool catalog, bypassing this transform.
The result is deduplicated by name so that only the highest version
of each tool is returned matching what protocol handlers expose
on the wire.
Args:
ctx: The current request context.
run_middleware: Whether to run middleware on the inner call.
@ -180,9 +185,10 @@ class CatalogTransform(Transform):
"""
token = self._bypass.set(True)
try:
return await ctx.fastmcp.list_tools(run_middleware=run_middleware)
tools = await ctx.fastmcp.list_tools(run_middleware=run_middleware)
finally:
self._bypass.reset(token)
return dedupe_with_versions(tools, lambda t: t.name)
async def get_resource_catalog(
self, ctx: Context, *, run_middleware: bool = True

View file

@ -14,15 +14,18 @@ Examples:
from __future__ import annotations
from collections.abc import Callable, Sequence
from dataclasses import dataclass
from functools import total_ordering
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Any, TypeVar, cast
from packaging.version import InvalidVersion, Version
if TYPE_CHECKING:
from fastmcp.utilities.components import FastMCPComponent
C = TypeVar("C", bound=Any)
@dataclass
class VersionSpec:
@ -283,3 +286,48 @@ def min_version(a: str | None, b: str | None) -> str | None:
if b is None:
return a
return a if compare_versions(a, b) <= 0 else b
def dedupe_with_versions(
components: Sequence[C],
key_fn: Callable[[C], str],
) -> list[C]:
"""Deduplicate components by key, keeping highest version.
Groups components by key, selects the highest version from each group,
and injects available versions into meta if any component is versioned.
Args:
components: Sequence of components to deduplicate.
key_fn: Function to extract the grouping key from a component.
Returns:
Deduplicated list with versions injected into meta.
"""
by_key: dict[str, list[C]] = {}
for c in components:
by_key.setdefault(key_fn(c), []).append(c)
result: list[C] = []
for versions in by_key.values():
highest: C = cast(C, max(versions, key=version_sort_key))
if any(c.version is not None for c in versions):
all_versions = sorted(
[c.version for c in versions if c.version is not None],
key=parse_version_key,
reverse=True,
)
meta = highest.meta or {}
highest = highest.model_copy(
update={
"meta": {
**meta,
"fastmcp": {
**meta.get("fastmcp", {}),
"versions": all_versions,
},
}
}
)
result.append(highest)
return result

View file

@ -17,7 +17,7 @@ _fastmcp_src_dir = (
)
@pytest.mark.timeout(10)
@pytest.mark.timeout(60)
@pytest.mark.client_process
@pytest.mark.skipif(
sys.platform == "win32",
@ -54,7 +54,7 @@ async def test_uv_transport():
assert sum == 3
@pytest.mark.timeout(10)
@pytest.mark.timeout(60)
@pytest.mark.client_process
@pytest.mark.skipif(
sys.platform == "win32",

View file

@ -5,7 +5,7 @@ from typing import Any
import pytest
from mcp.types import ImageContent, TextContent
from fastmcp import FastMCP
from fastmcp import Client, FastMCP
from fastmcp.exceptions import ToolError
from fastmcp.experimental.transforms.code_mode import (
CodeMode,
@ -507,6 +507,32 @@ async def test_code_mode_search_respects_disabled_tool_visibility() -> None:
assert "secret" not in text or "No tools" in text
async def test_code_mode_execute_sees_mid_run_visibility_changes() -> None:
"""Unlocking a tool mid-execution makes it callable in the same run."""
mcp = FastMCP("CodeMode Unlock")
@mcp.tool
async def unlock(ctx: Context) -> str:
await ctx.enable_components(names={"secret"}, components={"tool"})
return "unlocked"
@mcp.tool
async def secret() -> str:
return "secret-ok"
mcp.disable(names={"secret"}, components={"tool"})
mcp.add_transform(CodeMode(sandbox_provider=_UnsafeTestSandboxProvider()))
async with Client(mcp) as client:
result = await client.call_tool(
"execute",
{
"code": "await call_tool('unlock', {})\nreturn await call_tool('secret', {})"
},
)
assert result.data == {"result": "secret-ok"}
async def test_code_mode_execute_respects_tool_auth() -> None:
mcp = FastMCP("CodeMode Auth")

View file

@ -289,8 +289,8 @@ class TestConsentPageServerIcon:
class TestConsentCSPPolicy:
"""Tests for Content Security Policy customization on consent page."""
async def test_default_csp_includes_form_action(self):
"""Test that default CSP includes form-action directive."""
async def test_default_csp_omits_form_action(self):
"""Test that default CSP omits form-action to avoid Chrome redirect chain issues."""
verifier = Mock(spec=TokenVerifier)
verifier.required_scopes = ["read"]
@ -335,9 +335,9 @@ class TestConsentCSPPolicy:
response = client.get(f"/consent?txn_id={txn_id}")
assert response.status_code == 200
# Default CSP should be present with form-action
# Default CSP should be present but WITHOUT form-action
assert 'http-equiv="Content-Security-Policy"' in response.text
assert "form-action" in response.text
assert "form-action" not in response.text
async def test_empty_csp_disables_csp_meta_tag(self):
"""Test that empty string CSP disables CSP meta tag entirely."""

View file

@ -1,5 +1,6 @@
"""Tests for middleware support during initialization."""
from collections.abc import Sequence
from typing import Any
import mcp.types as mt
@ -9,6 +10,7 @@ from mcp.types import ErrorData, TextContent
from fastmcp import Client, FastMCP
from fastmcp.server.middleware import CallNext, Middleware, MiddlewareContext
from fastmcp.tools.tool import Tool
class InitializationMiddleware(Middleware):
@ -28,8 +30,8 @@ class InitializationMiddleware(Middleware):
async def on_initialize(
self,
context: MiddlewareContext[mt.InitializeRequest],
call_next: CallNext[mt.InitializeRequest, None],
) -> None:
call_next: CallNext[mt.InitializeRequest, mt.InitializeResult | None],
) -> mt.InitializeResult | None:
"""Capture initialization details."""
self.initialized = True
@ -66,8 +68,8 @@ class ClientDetectionMiddleware(Middleware):
async def on_initialize(
self,
context: MiddlewareContext[mt.InitializeRequest],
call_next: CallNext[mt.InitializeRequest, None],
) -> None:
call_next: CallNext[mt.InitializeRequest, mt.InitializeResult | None],
) -> mt.InitializeResult | None:
"""Detect test client during initialization."""
self.initialization_called = True
@ -80,8 +82,8 @@ class ClientDetectionMiddleware(Middleware):
async def on_list_tools(
self,
context: MiddlewareContext[mt.ListToolsRequest],
call_next: CallNext[mt.ListToolsRequest, list],
) -> list:
call_next: CallNext[mt.ListToolsRequest, Sequence[Tool]],
) -> Sequence[Tool]:
"""Modify tools based on client detection."""
tools = await call_next(context)
@ -112,8 +114,8 @@ async def test_simple_initialization_hook():
async def on_initialize(
self,
context: MiddlewareContext[mt.InitializeRequest],
call_next: CallNext[mt.InitializeRequest, None],
) -> None:
call_next: CallNext[mt.InitializeRequest, mt.InitializeResult | None],
) -> mt.InitializeResult | None:
self.called = True
return await call_next(context)
@ -304,8 +306,8 @@ async def test_middleware_mcp_error_during_initialization():
async def on_initialize(
self,
context: MiddlewareContext[mt.InitializeRequest],
call_next: CallNext[mt.InitializeRequest, None],
) -> None:
call_next: CallNext[mt.InitializeRequest, mt.InitializeResult | None],
) -> mt.InitializeResult | None:
raise McpError(
ErrorData(
code=mt.INVALID_PARAMS, message="Invalid initialization parameters"
@ -330,8 +332,8 @@ async def test_middleware_mcp_error_before_call_next():
async def on_initialize(
self,
context: MiddlewareContext[mt.InitializeRequest],
call_next: CallNext[mt.InitializeRequest, None],
) -> None:
call_next: CallNext[mt.InitializeRequest, mt.InitializeResult | None],
) -> mt.InitializeResult | None:
raise McpError(
ErrorData(code=mt.INVALID_REQUEST, message="Request validation failed")
)

View file

@ -134,3 +134,65 @@ class TestResourceUriPrefixing:
t for t in templates if t.uri_template == "resource://prefix/user/{user_id}"
)
assert template.name == "user_template"
class TestMountedResourceTemplateQueryParams:
"""Test that resource templates with query params work on mounted servers."""
async def test_mounted_template_with_query_param(self):
"""Query params in resource templates should work through mount."""
sub = FastMCP("Sub")
@sub.resource("resource://greet{?name}")
def greet(name: str = "World") -> str:
return f"Hello, {name}!"
main = FastMCP("Main")
main.mount(sub, "sub")
result = await main.read_resource("resource://sub/greet?name=Alice")
assert result.contents[0].content == "Hello, Alice!"
async def test_mounted_template_with_query_param_default(self):
"""Missing query params should use defaults through mount."""
sub = FastMCP("Sub")
@sub.resource("resource://greet{?name}")
def greet(name: str = "World") -> str:
return f"Hello, {name}!"
main = FastMCP("Main")
main.mount(sub, "sub")
result = await main.read_resource("resource://sub/greet")
assert result.contents[0].content == "Hello, World!"
async def test_mounted_template_with_multiple_query_params(self):
"""Multiple query params should all pass through mount correctly."""
sub = FastMCP("Sub")
@sub.resource("resource://data/{id}{?format,verbose}")
def get_data(id: str, format: str = "json", verbose: bool = False) -> str:
return f"id={id} format={format} verbose={verbose}"
main = FastMCP("Main")
main.mount(sub, "api")
result = await main.read_resource(
"resource://api/data/42?format=xml&verbose=true"
)
assert result.contents[0].content == "id=42 format=xml verbose=True"
async def test_mounted_template_with_partial_query_params(self):
"""Providing only some query params should use defaults for the rest."""
sub = FastMCP("Sub")
@sub.resource("resource://data/{id}{?format,limit}")
def get_data(id: str, format: str = "json", limit: int = 10) -> str:
return f"id={id} format={format} limit={limit}"
main = FastMCP("Main")
main.mount(sub, "api")
result = await main.read_resource("resource://api/data/42?limit=5")
assert result.contents[0].content == "id=42 format=json limit=5"

View file

@ -2,6 +2,7 @@
from __future__ import annotations
import ast
from collections.abc import Sequence
from mcp.types import TextContent
@ -10,9 +11,55 @@ from fastmcp import FastMCP
from fastmcp.server.context import Context
from fastmcp.server.transforms import GetToolNext
from fastmcp.server.transforms.catalog import CatalogTransform
from fastmcp.server.transforms.version_filter import VersionFilter
from fastmcp.tools.tool import Tool
from fastmcp.utilities.versions import VersionSpec
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _make_versioned_tool(name: str, version: str) -> Tool:
"""Create a tool with a specific version for testing."""
async def _fn() -> str:
return f"{name}@{version}"
return Tool.from_function(fn=_fn, name=name, version=version)
class CatalogReader(CatalogTransform):
"""Minimal CatalogTransform that exposes get_tool_catalog via a tool.
After calling the ``read_catalog`` tool, the full catalog is available
on ``self.last_catalog`` for assertions beyond tool names.
"""
def __init__(self) -> None:
super().__init__()
self.last_catalog: Sequence[Tool] = []
async def transform_tools(self, tools: Sequence[Tool]) -> Sequence[Tool]:
return [self._make_reader_tool()]
async def get_tool(
self, name: str, call_next: GetToolNext, *, version: VersionSpec | None = None
) -> Tool | None:
if name == "read_catalog":
return self._make_reader_tool()
return await call_next(name, version=version)
def _make_reader_tool(self) -> Tool:
transform = self
async def read_catalog(ctx: Context = None) -> list[str]: # type: ignore[assignment]
"""Return names of tools visible in the catalog."""
transform.last_catalog = await transform.get_tool_catalog(ctx)
return [t.name for t in transform.last_catalog]
return Tool.from_function(fn=read_catalog, name="read_catalog")
class ReplacingTransform(CatalogTransform):
"""Minimal subclass that replaces tools with a synthetic tool.
@ -80,3 +127,132 @@ class TestCatalogTransformBypass:
t2 = ReplacingTransform()
assert t1._instance_id != t2._instance_id
assert t1._bypass is not t2._bypass
class TestCatalogDeduplication:
"""get_tool_catalog() deduplicates versioned tools, keeping only the highest."""
async def test_returns_highest_version_only(self):
mcp = FastMCP("test")
mcp.add_tool(_make_versioned_tool("greet", "1"))
mcp.add_tool(_make_versioned_tool("greet", "2"))
mcp.add_tool(_make_versioned_tool("greet", "3"))
reader = CatalogReader()
mcp.add_transform(reader)
result = await mcp.call_tool("read_catalog", {})
names = _extract_result(result)
assert names == ["greet"]
async def test_version_metadata_injected(self):
"""Highest-version tool has meta.fastmcp.versions listing all available."""
mcp = FastMCP("test")
mcp.add_tool(_make_versioned_tool("greet", "1"))
mcp.add_tool(_make_versioned_tool("greet", "3"))
reader = CatalogReader()
mcp.add_transform(reader)
await mcp.call_tool("read_catalog", {})
assert len(reader.last_catalog) == 1
tool = reader.last_catalog[0]
assert tool.name == "greet"
assert tool.version == "3"
assert tool.meta is not None
versions = tool.meta["fastmcp"]["versions"]
assert versions == ["3", "1"]
async def test_mixed_versioned_and_unversioned(self):
mcp = FastMCP("test")
@mcp.tool
def standalone() -> str:
return "hi"
mcp.add_tool(_make_versioned_tool("greet", "1"))
mcp.add_tool(_make_versioned_tool("greet", "2"))
reader = CatalogReader()
mcp.add_transform(reader)
result = await mcp.call_tool("read_catalog", {})
names = sorted(_extract_result(result))
assert names == ["greet", "standalone"]
async def test_version_filter_applied_before_catalog(self):
"""A VersionFilter added before the CatalogTransform restricts what the catalog sees."""
mcp = FastMCP("test")
mcp.add_tool(_make_versioned_tool("greet", "1"))
mcp.add_tool(_make_versioned_tool("greet", "2"))
mcp.add_tool(_make_versioned_tool("greet", "3"))
# Filter keeps only versions < 3 — so v1 and v2 survive, v3 is excluded.
mcp.add_transform(VersionFilter(version_lt="3"))
reader = CatalogReader()
mcp.add_transform(reader)
await mcp.call_tool("read_catalog", {})
assert len(reader.last_catalog) == 1
tool = reader.last_catalog[0]
assert tool.name == "greet"
assert tool.version == "2"
class TestCatalogVisibility:
"""get_tool_catalog() respects visibility (disabled tools are excluded)."""
async def test_disabled_tool_excluded(self):
mcp = FastMCP("test")
@mcp.tool
def public() -> str:
return "visible"
@mcp.tool
def secret() -> str:
return "hidden"
mcp.disable(names={"secret"}, components={"tool"})
reader = CatalogReader()
mcp.add_transform(reader)
result = await mcp.call_tool("read_catalog", {})
names = _extract_result(result)
assert names == ["public"]
class TestCatalogAuth:
"""get_tool_catalog() respects tool-level auth filtering."""
async def test_auth_rejected_tool_excluded(self):
mcp = FastMCP("test")
@mcp.tool
def public() -> str:
return "visible"
@mcp.tool(auth=lambda _ctx: False)
def protected() -> str:
return "hidden"
reader = CatalogReader()
mcp.add_transform(reader)
result = await mcp.call_tool("read_catalog", {})
names = _extract_result(result)
assert names == ["public"]
# ---------------------------------------------------------------------------
# Test helpers
# ---------------------------------------------------------------------------
def _extract_result(result: object) -> list[str]:
"""Extract the list of names from a call_tool ToolResult."""
for c in result.content: # type: ignore[union-attr]
if isinstance(c, TextContent):
return ast.literal_eval(c.text)
raise AssertionError("No text content found")

View file

@ -5,6 +5,7 @@ import logging
import os
import sys
import tempfile
import time
from collections.abc import AsyncGenerator
from datetime import timedelta
from pathlib import Path
@ -339,11 +340,26 @@ async def test_multi_client_parallel_calls(tmp_path: Path):
assert all(len(result) == 2 for result in results) # type: ignore[arg-type]
async def _wait_for_process_exit(pid: int, timeout: float = 3.0) -> None:
"""Poll until a process has exited, raising if it's still alive after timeout."""
deadline = time.monotonic() + timeout
while time.monotonic() < deadline:
try:
psutil.Process(pid)
except psutil.NoSuchProcess:
return
await asyncio.sleep(0.05)
# Final check — if still alive, let the NoSuchProcess propagation fail the test clearly
psutil.Process(pid)
pytest.fail(f"Process {pid} still alive after {timeout}s")
@pytest.mark.skipif(
running_under_debugger(),
reason="Debugger holds a reference to the transport",
)
@pytest.mark.timeout(5)
@pytest.mark.timeout(15)
async def test_multi_client_lifespan(tmp_path: Path):
pid_1: int | None = None
pid_2: int | None = None
@ -393,18 +409,13 @@ async def test_multi_client_lifespan(tmp_path: Path):
gc_collect_harder()
# This test will fail while debugging because the debugger holds a reference to the underlying transport
with pytest.raises(psutil.NoSuchProcess):
while True:
psutil.Process(pid_1)
await asyncio.sleep(0.01)
with pytest.raises(psutil.NoSuchProcess):
while True:
psutil.Process(pid_2)
await asyncio.sleep(0.01)
assert pid_1 is not None
assert pid_2 is not None
await _wait_for_process_exit(pid_1)
await _wait_for_process_exit(pid_2)
@pytest.mark.timeout(15)
async def test_multi_client_force_close(tmp_path: Path):
server_script = inspect.cleandoc("""
from fastmcp import FastMCP
@ -446,15 +457,8 @@ async def test_multi_client_force_close(tmp_path: Path):
gc_collect_harder()
with pytest.raises(psutil.NoSuchProcess):
process = psutil.Process(pid_1)
assert not process
with pytest.raises(psutil.NoSuchProcess):
process = psutil.Process(pid_2)
assert not process
await _wait_for_process_exit(pid_1)
await _wait_for_process_exit(pid_2)
async def test_remote_config_default_no_auth():

38
uv.lock generated
View file

@ -895,7 +895,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.15" },
{ name = "ty", specifier = ">=0.0.20" },
]
[[package]]
@ -2978,26 +2978,26 @@ wheels = [
[[package]]
name = "ty"
version = "0.0.17"
version = "0.0.20"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/66/c3/41ae6346443eedb65b96761abfab890a48ce2aa5a8a27af69c5c5d99064d/ty-0.0.17.tar.gz", hash = "sha256:847ed6c120913e280bf9b54d8eaa7a1049708acb8824ad234e71498e8ad09f97", size = 5167209, upload-time = "2026-02-13T13:26:36.835Z" }
sdist = { url = "https://files.pythonhosted.org/packages/56/95/8de69bb98417227b01f1b1d743c819d6456c9fd140255b6124b05b17dfd6/ty-0.0.20.tar.gz", hash = "sha256:ebba6be7974c14efbb2a9adda6ac59848f880d7259f089dfa72a093039f1dcc6", size = 5262529, upload-time = "2026-03-02T15:51:36.587Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c0/01/0ef15c22a1c54b0f728ceff3f62d478dbf8b0dcf8ff7b80b954f79584f3e/ty-0.0.17-py3-none-linux_armv6l.whl", hash = "sha256:64a9a16555cc8867d35c2647c2f1afbd3cae55f68fd95283a574d1bb04fe93e0", size = 10192793, upload-time = "2026-02-13T13:27:13.943Z" },
{ url = "https://files.pythonhosted.org/packages/0f/2c/f4c322d9cded56edc016b1092c14b95cf58c8a33b4787316ea752bb9418e/ty-0.0.17-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:eb2dbd8acd5c5a55f4af0d479523e7c7265a88542efe73ed3d696eb1ba7b6454", size = 10051977, upload-time = "2026-02-13T13:26:57.741Z" },
{ url = "https://files.pythonhosted.org/packages/4c/a5/43746c1ff81e784f5fc303afc61fe5bcd85d0fcf3ef65cb2cef78c7486c7/ty-0.0.17-py3-none-macosx_11_0_arm64.whl", hash = "sha256:f18f5fd927bc628deb9ea2df40f06b5f79c5ccf355db732025a3e8e7152801f6", size = 9564639, upload-time = "2026-02-13T13:26:42.781Z" },
{ url = "https://files.pythonhosted.org/packages/d6/b8/280b04e14a9c0474af574f929fba2398b5e1c123c1e7735893b4cd73d13c/ty-0.0.17-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5383814d1d7a5cc53b3b07661856bab04bb2aac7a677c8d33c55169acdaa83df", size = 10061204, upload-time = "2026-02-13T13:27:00.152Z" },
{ url = "https://files.pythonhosted.org/packages/2a/d7/493e1607d8dfe48288d8a768a2adc38ee27ef50e57f0af41ff273987cda0/ty-0.0.17-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9c20423b8744b484f93e7bf2ef8a9724bca2657873593f9f41d08bd9f83444c9", size = 10013116, upload-time = "2026-02-13T13:26:34.543Z" },
{ url = "https://files.pythonhosted.org/packages/80/ef/22f3ed401520afac90dbdf1f9b8b7755d85b0d5c35c1cb35cf5bd11b59c2/ty-0.0.17-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e6f5b1aba97db9af86517b911674b02f5bc310750485dc47603a105bd0e83ddd", size = 10533623, upload-time = "2026-02-13T13:26:31.449Z" },
{ url = "https://files.pythonhosted.org/packages/75/ce/744b15279a11ac7138832e3a55595706b4a8a209c9f878e3ab8e571d9032/ty-0.0.17-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:488bce1a9bea80b851a97cd34c4d2ffcd69593d6c3f54a72ae02e5c6e47f3d0c", size = 11069750, upload-time = "2026-02-13T13:26:48.638Z" },
{ url = "https://files.pythonhosted.org/packages/f2/be/1133c91f15a0e00d466c24f80df486d630d95d1b2af63296941f7473812f/ty-0.0.17-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8df66b91ec84239420985ec215e7f7549bfda2ac036a3b3c065f119d1c06825a", size = 10870862, upload-time = "2026-02-13T13:26:54.715Z" },
{ url = "https://files.pythonhosted.org/packages/3e/4a/a2ed209ef215b62b2d3246e07e833081e07d913adf7e0448fc204be443d6/ty-0.0.17-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:002139e807c53002790dfefe6e2f45ab0e04012e76db3d7c8286f96ec121af8f", size = 10628118, upload-time = "2026-02-13T13:26:45.439Z" },
{ url = "https://files.pythonhosted.org/packages/b3/0c/87476004cb5228e9719b98afffad82c3ef1f84334bde8527bcacba7b18cb/ty-0.0.17-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:6c4e01f05ce82e5d489ab3900ca0899a56c4ccb52659453780c83e5b19e2b64c", size = 10038185, upload-time = "2026-02-13T13:27:02.693Z" },
{ url = "https://files.pythonhosted.org/packages/46/4b/98f0b3ba9aef53c1f0305519536967a4aa793a69ed72677b0a625c5313ac/ty-0.0.17-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:2b226dd1e99c0d2152d218c7e440150d1a47ce3c431871f0efa073bbf899e881", size = 10047644, upload-time = "2026-02-13T13:27:05.474Z" },
{ url = "https://files.pythonhosted.org/packages/93/e0/06737bb80aa1a9103b8651d2eb691a7e53f1ed54111152be25f4a02745db/ty-0.0.17-py3-none-musllinux_1_2_i686.whl", hash = "sha256:8b11f1da7859e0ad69e84b3c5ef9a7b055ceed376a432fad44231bdfc48061c2", size = 10231140, upload-time = "2026-02-13T13:27:10.844Z" },
{ url = "https://files.pythonhosted.org/packages/7c/79/e2a606bd8852383ba9abfdd578f4a227bd18504145381a10a5f886b4e751/ty-0.0.17-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:c04e196809ff570559054d3e011425fd7c04161529eb551b3625654e5f2434cb", size = 10718344, upload-time = "2026-02-13T13:26:51.66Z" },
{ url = "https://files.pythonhosted.org/packages/c5/2d/2663984ac11de6d78f74432b8b14ba64d170b45194312852b7543cf7fd56/ty-0.0.17-py3-none-win32.whl", hash = "sha256:305b6ed150b2740d00a817b193373d21f0767e10f94ac47abfc3b2e5a5aec809", size = 9672932, upload-time = "2026-02-13T13:27:08.522Z" },
{ url = "https://files.pythonhosted.org/packages/de/b5/39be78f30b31ee9f5a585969930c7248354db90494ff5e3d0756560fb731/ty-0.0.17-py3-none-win_amd64.whl", hash = "sha256:531828267527aee7a63e972f54e5eee21d9281b72baf18e5c2850c6b862add83", size = 10542138, upload-time = "2026-02-13T13:27:17.084Z" },
{ url = "https://files.pythonhosted.org/packages/40/b7/f875c729c5d0079640c75bad2c7e5d43edc90f16ba242f28a11966df8f65/ty-0.0.17-py3-none-win_arm64.whl", hash = "sha256:de9810234c0c8d75073457e10a84825b9cd72e6629826b7f01c7a0b266ae25b1", size = 10023068, upload-time = "2026-02-13T13:26:39.637Z" },
{ url = "https://files.pythonhosted.org/packages/0b/2c/718abe48393e521bf852cd6b0f984766869b09c258d6e38a118768a91731/ty-0.0.20-py3-none-linux_armv6l.whl", hash = "sha256:7cc12769c169c9709a829c2248ee2826b7aae82e92caeac813d856f07c021eae", size = 10333656, upload-time = "2026-03-02T15:51:56.461Z" },
{ url = "https://files.pythonhosted.org/packages/41/0e/eb1c4cc4a12862e2327b72657bcebb10b7d9f17046f1bdcd6457a0211615/ty-0.0.20-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:3b777c1bf13bc0a95985ebb8a324b8668a4a9b2e514dde5ccf09e4d55d2ff232", size = 10168505, upload-time = "2026-03-02T15:51:51.895Z" },
{ url = "https://files.pythonhosted.org/packages/89/7f/10230798e673f0dd3094dfd16e43bfd90e9494e7af6e8e7db516fb431ddf/ty-0.0.20-py3-none-macosx_11_0_arm64.whl", hash = "sha256:b2a4a7db48bf8cba30365001bc2cad7fd13c1a5aacdd704cc4b7925de8ca5eb3", size = 9678510, upload-time = "2026-03-02T15:51:48.451Z" },
{ url = "https://files.pythonhosted.org/packages/7a/3d/59d9159577494edd1728f7db77b51bb07884bd21384f517963114e3ab5f6/ty-0.0.20-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6846427b8b353a43483e9c19936dc6a25612573b44c8f7d983dfa317e7f00d4c", size = 10162926, upload-time = "2026-03-02T15:51:40.558Z" },
{ url = "https://files.pythonhosted.org/packages/9c/a8/b7273eec3e802f78eb913fbe0ce0c16ef263723173e06a5776a8359b2c66/ty-0.0.20-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:245ceef5bd88df366869385cf96411cb14696334f8daa75597cf7e41c3012eb8", size = 10171702, upload-time = "2026-03-02T15:51:44.069Z" },
{ url = "https://files.pythonhosted.org/packages/9f/32/5f1144f2f04a275109db06e3498450c4721554215b80ae73652ef412eeab/ty-0.0.20-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c4d21d1cdf67a444d3c37583c17291ddba9382a9871021f3f5d5735e09e85efe", size = 10682552, upload-time = "2026-03-02T15:51:33.102Z" },
{ url = "https://files.pythonhosted.org/packages/6a/db/9f1f637310792f12bd6ed37d5fc8ab39ba1a9b0c6c55a33865e9f1cad840/ty-0.0.20-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bd4ffd907d1bd70e46af9e9a2f88622f215e1bf44658ea43b32c2c0b357299e4", size = 11242605, upload-time = "2026-03-02T15:51:34.895Z" },
{ url = "https://files.pythonhosted.org/packages/1a/68/cc9cae2e732fcfd20ccdffc508407905a023fc8493b8771c392d915528dc/ty-0.0.20-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b6594b58d8b0e9d16a22b3045fc1305db4b132c8d70c17784ab8c7a7cc986807", size = 10974655, upload-time = "2026-03-02T15:51:46.011Z" },
{ url = "https://files.pythonhosted.org/packages/1c/c1/b9e3e3f28fe63486331e653f6aeb4184af8b1fe80542fcf74d2dda40a93d/ty-0.0.20-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3662f890518ce6cf4d7568f57d03906912d2afbf948a01089a28e325b1ef198c", size = 10761325, upload-time = "2026-03-02T15:51:26.818Z" },
{ url = "https://files.pythonhosted.org/packages/39/9e/67db935bdedf219a00fb69ec5437ba24dab66e0f2e706dd54a4eca234b84/ty-0.0.20-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:0e3ffbae58f9f0d17cdc4ac6d175ceae560b7ed7d54f9ddfb1c9f31054bcdc2c", size = 10145793, upload-time = "2026-03-02T15:51:38.562Z" },
{ url = "https://files.pythonhosted.org/packages/c7/de/b0eb815d4dc5a819c7e4faddc2a79058611169f7eef07ccc006531ce228c/ty-0.0.20-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:176e52bc8bb00b0e84efd34583962878a447a3a0e34ecc45fd7097a37554261b", size = 10189640, upload-time = "2026-03-02T15:51:50.202Z" },
{ url = "https://files.pythonhosted.org/packages/b8/71/63734923965cbb70df1da3e93e4b8875434e326b89e9f850611122f279bf/ty-0.0.20-py3-none-musllinux_1_2_i686.whl", hash = "sha256:b2bc73025418e976ca4143dde71fb9025a90754a08ac03e6aa9b80d4bed1294b", size = 10370568, upload-time = "2026-03-02T15:51:42.295Z" },
{ url = "https://files.pythonhosted.org/packages/32/a0/a532c2048533347dff48e9ca98bd86d2c224356e101688a8edaf8d6973fb/ty-0.0.20-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:d52f7c9ec6e363e094b3c389c344d5a140401f14a77f0625e3f28c21918552f5", size = 10853999, upload-time = "2026-03-02T15:51:58.963Z" },
{ url = "https://files.pythonhosted.org/packages/48/88/36c652c658fe96658043e4abc8ea97801de6fb6e63ab50aaa82807bff1d8/ty-0.0.20-py3-none-win32.whl", hash = "sha256:c7d32bfe93f8fcaa52b6eef3f1b930fd7da410c2c94e96f7412c30cfbabf1d17", size = 9744206, upload-time = "2026-03-02T15:51:54.183Z" },
{ url = "https://files.pythonhosted.org/packages/ff/a7/a4a13bed1d7fd9d97aaa3c5bb5e6d3e9a689e6984806cbca2ab4c9233cac/ty-0.0.20-py3-none-win_amd64.whl", hash = "sha256:a5e10f40fc4a0a1cbcb740a4aad5c7ce35d79f030836ea3183b7a28f43170248", size = 10711999, upload-time = "2026-03-02T15:51:29.212Z" },
{ url = "https://files.pythonhosted.org/packages/8d/7e/6bfd748a9f4ff9267ed3329b86a0f02cdf6ab49f87bc36c8a164852f99fc/ty-0.0.20-py3-none-win_arm64.whl", hash = "sha256:53f7a5c12c960e71f160b734f328eff9a35d578af4b67a36b0bb5990ac5cdc27", size = 10150143, upload-time = "2026-03-02T15:51:31.283Z" },
]
[[package]]