mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-26 23:44:17 +02:00
commit
ec7a83f5f9
38 changed files with 664 additions and 215 deletions
15
.github/workflows/run-upgrade-checks.yml
vendored
15
.github/workflows/run-upgrade-checks.yml
vendored
|
|
@ -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.*
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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 < 3.0 (v1 and v2)
|
||||
# Versions < 3.0 (v1 and v2)
|
||||
server.add_transform(VersionFilter(version_lt="3.0"))
|
||||
|
||||
# Versions >= 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
68
docs/unify-intent.js
Normal 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 });
|
||||
})();
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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}")
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
||||
|
|
|
|||
|
|
@ -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."""
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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
38
uv.lock
generated
|
|
@ -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]]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue