From 794bfe95677900057e4cf7a54c51adc85b9563e7 Mon Sep 17 00:00:00 2001 From: Eduardo Cruz Guedes Date: Mon, 27 Jul 2026 16:12:15 -0300 Subject: [PATCH 01/53] Add `valid_scopes` parameter to OIDC proxy valid scopes (#4660) * Accept valid_scopes on OIDCProxy * Keep valid_scopes when verify_id_token restores scopes * Document valid_scopes on the OIDC proxy --------- Co-authored-by: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> --- docs/servers/auth/oauth-proxy.mdx | 8 +- docs/servers/auth/oidc-proxy.mdx | 9 ++ .../fastmcp/server/auth/oidc_proxy.py | 29 ++-- tests/server/auth/test_oidc_proxy.py | 136 ++++++++++++++++++ 4 files changed, 171 insertions(+), 11 deletions(-) diff --git a/docs/servers/auth/oauth-proxy.mdx b/docs/servers/auth/oauth-proxy.mdx index 0ca8b524c..5cc0f633a 100644 --- a/docs/servers/auth/oauth-proxy.mdx +++ b/docs/servers/auth/oauth-proxy.mdx @@ -206,9 +206,11 @@ mcp = FastMCP(name="My Server", auth=auth) - List of all possible valid scopes for the OAuth provider. These are advertised - to clients through the `/.well-known` endpoints. Defaults to `required_scopes` - from your TokenVerifier if not specified. + The complete set of scopes clients are allowed to request — the full set of + available scopes (a superset of `required_scopes`). These are advertised to + clients through the `/.well-known` endpoints and enforced at Dynamic Client + Registration. Defaults to `required_scopes` from your TokenVerifier if not + specified. diff --git a/docs/servers/auth/oidc-proxy.mdx b/docs/servers/auth/oidc-proxy.mdx index 8580c28fd..763963858 100644 --- a/docs/servers/auth/oidc-proxy.mdx +++ b/docs/servers/auth/oidc-proxy.mdx @@ -132,6 +132,15 @@ These patterns apply to MCP client loopback redirects. Configure the upstream OA + + The complete set of scopes clients are allowed to request — the full set of + available scopes (a superset of `required_scopes`). These are advertised to + clients through the `/.well-known` endpoints (as `scopes_supported`) and + enforced at Dynamic Client Registration: a client registering with a scope + outside this set is rejected. Defaults to `required_scopes` from your token + verifier if not specified. + + Token endpoint authentication method for the upstream OAuth server. Controls how the proxy authenticates when exchanging authorization codes and refresh tokens with the upstream provider. - `"client_secret_basic"`: Send credentials in Authorization header (most common) diff --git a/fastmcp_slim/fastmcp/server/auth/oidc_proxy.py b/fastmcp_slim/fastmcp/server/auth/oidc_proxy.py index 8f17c75b8..e2bda58ab 100644 --- a/fastmcp_slim/fastmcp/server/auth/oidc_proxy.py +++ b/fastmcp_slim/fastmcp/server/auth/oidc_proxy.py @@ -226,6 +226,7 @@ class OIDCProxy(OAuthProxy): redirect_path: str | None = None, # Client configuration allowed_client_redirect_uris: list[str] | None = None, + valid_scopes: list[str] | None = None, client_storage: AsyncKeyValue | None = None, # JWT and encryption keys jwt_signing_key: str | bytes | None = None, @@ -284,6 +285,15 @@ class OIDCProxy(OAuthProxy): ports allowed to vary for MCP compatibility. Unsafe browser schemes are rejected. If empty list, no redirect URIs are allowed. These are for MCP clients performing loopback redirects, NOT for the upstream OAuth app. + valid_scopes: The complete set of scopes clients are allowed to request, + advertised to clients via the `/.well-known` endpoints (as + `scopes_supported`) and enforced at Dynamic Client Registration: a + client that registers requesting a scope outside this set is rejected. + This is a superset of `required_scopes`, which is only the floor + enforced during token validation. Defaults to `required_scopes` when + not provided, so permitting optional scopes beyond the required floor + means setting this explicitly. Valid whether or not a custom + `token_verifier` is supplied. client_storage: Storage backend for OAuth state (client registrations, encrypted tokens). If None, an encrypted file store will be created in the data directory (derived from `platformdirs`). @@ -414,6 +424,7 @@ class OIDCProxy(OAuthProxy): "issuer_url": issuer_url or base_url, "service_documentation_url": self.oidc_config.service_documentation, "allowed_client_redirect_uris": allowed_client_redirect_uris, + "valid_scopes": valid_scopes, "client_storage": client_storage, "jwt_signing_key": jwt_signing_key, "token_endpoint_auth_method": token_endpoint_auth_method, @@ -454,14 +465,16 @@ class OIDCProxy(OAuthProxy): self._verify_id_token = verify_id_token - # When verify_id_token strips scopes from the verifier, restore - # them on the provider so they're still advertised to clients - # and enforced at the FastMCP token level. We also need to - # recompute derived state that OAuthProxy.__init__ already built - # from the (empty) verifier scopes. - if verify_id_token and required_scopes: - self.required_scopes = required_scopes - self.update_default_scopes(required_scopes) + # When verify_id_token strips scopes from the verifier, restore the + # derived scope state OAuthProxy.__init__ built from the (empty) verifier + # scopes. required_scopes is the enforcement floor; the advertised and + # registerable set is the broader valid_scopes when one was given. + if verify_id_token: + if required_scopes: + self.required_scopes = required_scopes + advertised_scopes = valid_scopes or required_scopes + if advertised_scopes: + self.update_default_scopes(advertised_scopes) def _get_verification_token( self, upstream_token_set: UpstreamTokenSet diff --git a/tests/server/auth/test_oidc_proxy.py b/tests/server/auth/test_oidc_proxy.py index 2636a1c83..755049120 100644 --- a/tests/server/auth/test_oidc_proxy.py +++ b/tests/server/auth/test_oidc_proxy.py @@ -982,3 +982,139 @@ class TestDiscoveryTimeout: ), ) assert timeout == 10 + + +class TestOIDCProxyValidScopes: + """Tests for the valid_scopes parameter on OIDCProxy.""" + + def test_valid_scopes_widens_advertised_set(self, valid_oidc_configuration_dict): + """valid_scopes broadens the advertised/registerable set beyond required_scopes.""" + with patch( + "fastmcp.server.auth.oidc_proxy.OIDCConfiguration.get_oidc_configuration" + ) as mock_get: + oidc_config = OIDCConfiguration.model_validate( + valid_oidc_configuration_dict + ) + mock_get.return_value = oidc_config + + proxy = OIDCProxy( + config_url=TEST_CONFIG_URL, + client_id=TEST_CLIENT_ID, + client_secret=TEST_CLIENT_SECRET, + base_url=TEST_BASE_URL, + required_scopes=["openid"], + valid_scopes=["openid", "email", "calendar"], + jwt_signing_key="test-secret", + ) + + assert proxy.client_registration_options is not None + assert proxy.client_registration_options.valid_scopes == [ + "openid", + "email", + "calendar", + ] + assert proxy.client_registration_options.default_scopes == [ + "openid", + "email", + "calendar", + ] + assert proxy._default_scope_str == "openid email calendar" + + def test_valid_scopes_defaults_to_required_scopes( + self, valid_oidc_configuration_dict + ): + """Omitting valid_scopes falls back to required_scopes (existing behavior).""" + with patch( + "fastmcp.server.auth.oidc_proxy.OIDCConfiguration.get_oidc_configuration" + ) as mock_get: + oidc_config = OIDCConfiguration.model_validate( + valid_oidc_configuration_dict + ) + mock_get.return_value = oidc_config + + proxy = OIDCProxy( + config_url=TEST_CONFIG_URL, + client_id=TEST_CLIENT_ID, + client_secret=TEST_CLIENT_SECRET, + base_url=TEST_BASE_URL, + required_scopes=["read", "write"], + jwt_signing_key="test-secret", + ) + + assert proxy.client_registration_options is not None + assert proxy.client_registration_options.valid_scopes == ["read", "write"] + + def test_valid_scopes_with_custom_token_verifier( + self, valid_oidc_configuration_dict + ): + """valid_scopes is allowed alongside a custom token_verifier (no error).""" + with patch( + "fastmcp.server.auth.oidc_proxy.OIDCConfiguration.get_oidc_configuration" + ) as mock_get: + oidc_config = OIDCConfiguration.model_validate( + valid_oidc_configuration_dict + ) + mock_get.return_value = oidc_config + + custom_verifier = IntrospectionTokenVerifier( + introspection_url="https://example.com/oauth/introspect", + client_id="introspection-client", + client_secret="introspection-secret", + required_scopes=["read"], + ) + + proxy = OIDCProxy( + config_url=TEST_CONFIG_URL, + client_id=TEST_CLIENT_ID, + client_secret=TEST_CLIENT_SECRET, + base_url=TEST_BASE_URL, + token_verifier=custom_verifier, + valid_scopes=["read", "write", "admin"], + jwt_signing_key="test-secret", + ) + + assert proxy.client_registration_options is not None + assert proxy.client_registration_options.valid_scopes == [ + "read", + "write", + "admin", + ] + + def test_valid_scopes_preserved_with_verify_id_token( + self, valid_oidc_configuration_dict + ): + """verify_id_token restores required_scopes without clobbering valid_scopes.""" + with patch( + "fastmcp.server.auth.oidc_proxy.OIDCConfiguration.get_oidc_configuration" + ) as mock_get: + oidc_config = OIDCConfiguration.model_validate( + valid_oidc_configuration_dict + ) + mock_get.return_value = oidc_config + + proxy = OIDCProxy( + config_url=TEST_CONFIG_URL, + client_id=TEST_CLIENT_ID, + client_secret=TEST_CLIENT_SECRET, + base_url=TEST_BASE_URL, + required_scopes=["read"], + valid_scopes=["read", "write", "admin"], + verify_id_token=True, + jwt_signing_key="test-secret", + ) + + # Enforcement floor is restored to required_scopes... + assert proxy.required_scopes == ["read"] + # ...but the advertised/registerable set keeps the full valid_scopes. + assert proxy.client_registration_options is not None + assert proxy.client_registration_options.valid_scopes == [ + "read", + "write", + "admin", + ] + assert proxy.client_registration_options.default_scopes == [ + "read", + "write", + "admin", + ] + assert proxy._default_scope_str == "read write admin" From ffea4d6a3e5a3210a46d03dfb3e6bcd2f94c0fe1 Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Mon, 27 Jul 2026 15:13:03 -0400 Subject: [PATCH 02/53] Docs: add v3.4.5 changelog entries to main (#4674) * Docs: add v3.4.5 changelog entries * Condense the 3.4.5 entries to patch-release length --- docs/changelog.mdx | 20 ++++++++++++++++++++ docs/updates.mdx | 10 ++++++++++ 2 files changed, 30 insertions(+) diff --git a/docs/changelog.mdx b/docs/changelog.mdx index 063bc4646..c22d48e74 100644 --- a/docs/changelog.mdx +++ b/docs/changelog.mdx @@ -5,6 +5,26 @@ rss: true tag: NEW --- + + +**[v3.4.5: Key Change](https://github.com/PrefectHQ/fastmcp/releases/tag/v3.4.5)** + +FastMCP 3.4.5 collects five fixes for the 3.x line, led by `JWTVerifier` no longer rejecting every token when an authorization server publishes an unrecognized key type such as Ed25519. + +### Fixes 🐞 +* Backport #4517 to release/3.x: skip unsupported JWKS keys (#4515) by [@kakiii](https://github.com/kakiii) in [#4631](https://github.com/PrefectHQ/fastmcp/pull/4631) +* Backport #4469 to release/3.x: fix Azure scope fallback by [@jlowin](https://github.com/jlowin) in [#4662](https://github.com/PrefectHQ/fastmcp/pull/4662) +* Backport #4523 to release/3.x: serialize deep object query parameters by [@jlowin](https://github.com/jlowin) in [#4664](https://github.com/PrefectHQ/fastmcp/pull/4664) +* Backport #4564 to release/3.x: make transformed tool required order deterministic by [@jlowin](https://github.com/jlowin) in [#4665](https://github.com/PrefectHQ/fastmcp/pull/4665) +* Backport #4492 to release/3.x: don't mutate the caller's schema in compress_schema by [@jlowin](https://github.com/jlowin) in [#4663](https://github.com/PrefectHQ/fastmcp/pull/4663) + +## New Contributors +* @kakiii made their first contribution in [#4631](https://github.com/PrefectHQ/fastmcp/pull/4631) + +**Full Changelog**: [v3.4.4...v3.4.5](https://github.com/PrefectHQ/fastmcp/compare/v3.4.4...v3.4.5) + + + **[v3.4.4: Host in Translation](https://github.com/PrefectHQ/fastmcp/releases/tag/v3.4.4)** diff --git a/docs/updates.mdx b/docs/updates.mdx index 0faf12da9..47c0c8364 100644 --- a/docs/updates.mdx +++ b/docs/updates.mdx @@ -5,6 +5,16 @@ icon: "sparkles" tag: NEW --- + + +A maintenance release for the 3.x line. A single unrecognized JWKS key — Ed25519, which Rauthy and Ory Hydra publish by default — no longer poisons the entire key cache, alongside fixes for Azure scope fallback, OpenAPI `deepObject` query serialization, schema compression, and transformed tool `required` ordering. + + + Date: Tue, 28 Jul 2026 03:25:22 +0800 Subject: [PATCH 03/53] Fix OpenAPI allOf reference fields (#4653) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix OpenAPI allOf reference fields 🤖 Generated with Codex * Add allOf reference crash regression 🤖 Generated with Codex * Handle OpenAPI component refs in allOf 🤖 Generated with Codex --------- Co-authored-by: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> --- .../fastmcp/utilities/openapi/schemas.py | 52 ++++++-- .../openapi/test_openapi_features.py | 115 ++++++++++++++++++ 2 files changed, 159 insertions(+), 8 deletions(-) diff --git a/fastmcp_slim/fastmcp/utilities/openapi/schemas.py b/fastmcp_slim/fastmcp/utilities/openapi/schemas.py index 5980621c2..f272111b3 100644 --- a/fastmcp_slim/fastmcp/utilities/openapi/schemas.py +++ b/fastmcp_slim/fastmcp/utilities/openapi/schemas.py @@ -221,6 +221,43 @@ def _make_optional_parameter_nullable(schema: dict[str, Any]) -> dict[str, Any]: return schema +def _allof_members( + schema: dict[str, Any], + schema_defs: dict[str, Any], + resolving: set[str] | None = None, +) -> list[dict[str, Any]]: + """Expand local schema references while collecting ``allOf`` members.""" + resolving = resolving or set() + + ref = schema.get("$ref") + if isinstance(ref, str): + for prefix in ("#/$defs/", "#/components/schemas/"): + if ref.startswith(prefix): + name = ref.removeprefix(prefix) + referenced_schema = schema_defs.get(name) + if isinstance(referenced_schema, dict) and name not in resolving: + siblings = { + key: value for key, value in schema.items() if key != "$ref" + } + members = _allof_members( + referenced_schema, schema_defs, resolving | {name} + ) + return members + ([siblings] if siblings else []) + break + + all_of = schema.get("allOf") + if isinstance(all_of, list): + members = [] + for member in all_of: + if isinstance(member, dict): + members.extend(_allof_members(member, schema_defs, resolving)) + + siblings = {key: value for key, value in schema.items() if key != "allOf"} + return members + ([siblings] if siblings else []) + + return [schema] + + def _combine_schemas_and_map_params( route: HTTPRoute, convert_refs: bool = True, @@ -273,14 +310,13 @@ def _combine_schemas_and_map_params( merged_props = {} merged_required = [] - for sub_schema in body_schema["allOf"]: - if isinstance(sub_schema, dict): - # Merge properties - if "properties" in sub_schema: - merged_props.update(sub_schema["properties"]) - # Merge required fields - if "required" in sub_schema: - merged_required.extend(sub_schema["required"]) + for sub_schema in _allof_members(body_schema, route.request_schemas): + # Merge properties + if "properties" in sub_schema: + merged_props.update(sub_schema["properties"]) + # Merge required fields + if "required" in sub_schema: + merged_required.extend(sub_schema["required"]) # Update body_schema with merged properties body_schema["properties"] = merged_props diff --git a/tests/server/providers/openapi/test_openapi_features.py b/tests/server/providers/openapi/test_openapi_features.py index ceabe117d..35ddc4aa0 100644 --- a/tests/server/providers/openapi/test_openapi_features.py +++ b/tests/server/providers/openapi/test_openapi_features.py @@ -1,5 +1,6 @@ """Tests for OpenAPI feature support in OpenAPIProvider.""" +import json from typing import Any from unittest.mock import AsyncMock, Mock @@ -1412,3 +1413,117 @@ class TestMultipartUpload: assert "multipart/form-data" in received["content_type"] assert b"data" in received["body"] + + +class TestAllOfReferenceRequestBodies: + """Request bodies keep fields inherited through an allOf reference.""" + + SPEC = { + "openapi": "3.1.0", + "info": {"title": "Pet API", "version": "1.0.0"}, + "servers": [{"url": "https://api.example.com"}], + "paths": { + "/pets": { + "post": { + "operationId": "create_pet", + "requestBody": { + "required": True, + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Cat"} + } + }, + }, + "responses": {"200": {"description": "Created"}}, + } + } + }, + "components": { + "schemas": { + "Animal": { + "type": "object", + "properties": {"animalId": {"type": "string"}}, + "required": ["animalId"], + }, + "Pet": { + "allOf": [ + {"$ref": "#/components/schemas/Animal"}, + { + "type": "object", + "properties": {"petType": {"type": "string"}}, + "required": ["petType"], + }, + ] + }, + "Cat": { + "allOf": [ + {"$ref": "#/components/schemas/Pet"}, + { + "type": "object", + "properties": {"meowVolume": {"type": "integer"}}, + "required": ["meowVolume"], + }, + ] + }, + } + }, + } + + async def test_allof_reference_fields_reach_tool_schema_and_request_body(self): + received: dict[str, object] = {} + + def handler(request): + received["body"] = json.loads(request.content) + return httpx2.Response(200, json={"ok": True}) + + async with httpx2.AsyncClient( + transport=httpx2.MockTransport(handler), + base_url="https://api.example.com", + ) as client: + server = create_openapi_server(self.SPEC, client) + async with Client(server) as mcp_client: + tools = await mcp_client.list_tools() + tool = next(tool for tool in tools if tool.name == "create_pet") + assert tool.input_schema["properties"].keys() >= { + "animalId", + "petType", + "meowVolume", + } + + result = await mcp_client.call_tool( + "create_pet", + {"animalId": "a-1", "petType": "cat", "meowVolume": 11}, + ) + + assert result.structured_content == {"ok": True} + assert received["body"] == { + "animalId": "a-1", + "petType": "cat", + "meowVolume": 11, + } + + async def test_allof_reference_request_body_does_not_crash(self): + """Required fields inherited through a reference can be sent together.""" + received: dict[str, object] = {} + + def handler(request): + received["body"] = json.loads(request.content) + return httpx2.Response(200, json={"ok": True}) + + async with httpx2.AsyncClient( + transport=httpx2.MockTransport(handler), + base_url="https://api.example.com", + ) as client: + server = create_openapi_server(self.SPEC, client) + async with Client(server) as mcp_client: + result = await mcp_client.call_tool( + "create_pet", + {"animalId": "a-1", "petType": "cat", "meowVolume": 11}, + ) + + assert result.structured_content == {"ok": True} + assert received["body"] == { + "animalId": "a-1", + "petType": "cat", + "meowVolume": 11, + } From 75b9f9250443f1808fabe1193a1bf915cf6aef7e Mon Sep 17 00:00:00 2001 From: Bill Easton Date: Mon, 27 Jul 2026 15:05:29 -0500 Subject: [PATCH 04/53] feat: Add telemetry interop mode for FastMCP (#4046) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Claude Opus 4.7 Co-authored-by: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> --- dev-docs/v4-notes/change-register.md | 8 +- dev-docs/v4-notes/feature-program.md | 2 +- dev-docs/v4-notes/protocol-2026.md | 2 +- docs/more/settings.mdx | 2 +- docs/servers/telemetry.mdx | 45 ++- fastmcp_slim/fastmcp/server/telemetry.py | 56 +++- fastmcp_slim/fastmcp/settings.py | 45 +-- fastmcp_slim/fastmcp/telemetry.py | 87 +++++- tests/server/telemetry/test_server_tracing.py | 12 +- tests/telemetry/test_interop.py | 280 ++++++++++++++++++ 10 files changed, 494 insertions(+), 45 deletions(-) create mode 100644 tests/telemetry/test_interop.py diff --git a/dev-docs/v4-notes/change-register.md b/dev-docs/v4-notes/change-register.md index 771a2234c..89762a6ef 100644 --- a/dev-docs/v4-notes/change-register.md +++ b/dev-docs/v4-notes/change-register.md @@ -176,11 +176,13 @@ SDK v2 seeds an `OpenTelemetryMiddleware` into every lowlevel `Server`, so each *Verify:* `fastmcp_slim/fastmcp/server/low_level.py` (the `OpenTelemetryMiddleware` filter); `tests/server/telemetry/test_server_tracing.py::TestSingleServerSpan`. -### Telemetry on by default, with an explicit off-switch — Absorbed +### Telemetry on by default, with a three-way mode setting — Absorbed -FastMCP's OpenTelemetry instrumentation is on by default. Because FastMCP uses only the OpenTelemetry API, span creation is a no-op with negligible overhead (the API's `NonRecordingSpan`) unless the user configures an SDK and exporter — so being always-on costs nothing until you opt into collection. The new `FASTMCP_ENABLE_TELEMETRY` setting (`fastmcp.settings.enable_telemetry`, default `true`) is the explicit off-switch: set it to `false` and `get_tracer()` returns a genuine no-op tracer, so no FastMCP spans are created even when an SDK is configured. The off-switch governs FastMCP's own spans (all SERVER spans, plus FastMCP's high-level CLIENT span); the SDK's low-level `mcp-python-sdk` `MCP send ` CLIENT spans are governed by the user's OpenTelemetry SDK, not this setting. FastMCP's SERVER span now also carries `mcp.protocol.version` — the attribute the dropped SDK `OpenTelemetryMiddleware` set — restoring parity with the SDK's semantic conventions. +FastMCP's OpenTelemetry instrumentation is on by default. Because FastMCP uses only the OpenTelemetry API, span creation is a no-op with negligible overhead (the API's `NonRecordingSpan`) unless the user configures an SDK and exporter — so being always-on costs nothing until you opt into collection. `FASTMCP_TELEMETRY_MODE` (`fastmcp.settings.telemetry_mode`, default `native`) controls how much is active: `native` emits spans and propagates trace context; `propagation_only` emits no FastMCP spans but still extracts the incoming `_meta` context and attaches it, so downstream spans are parented to the calling trace; `off` is a full pass-through that touches neither spans nor context. The setting governs FastMCP's own spans (all SERVER spans, plus FastMCP's high-level CLIENT span); the SDK's low-level `mcp-python-sdk` `MCP send ` CLIENT spans are governed by the user's OpenTelemetry SDK, not this setting. `suppress_fastmcp_telemetry()` applies `propagation_only` semantics to a single block for library authors who own the MCP hierarchy for one operation rather than process-wide; it cannot override `off`. FastMCP's SERVER span now also carries `mcp.protocol.version` — the attribute the dropped SDK `OpenTelemetryMiddleware` set — restoring parity with the SDK's semantic conventions. -*Verify:* `fastmcp_slim/fastmcp/settings.py` (`enable_telemetry`); `fastmcp_slim/fastmcp/telemetry.py` (`get_tracer` off-switch); `fastmcp_slim/fastmcp/server/telemetry.py` (`get_protocol_span_attributes`); `tests/server/telemetry/test_server_tracing.py::TestTelemetryEnabledByDefault`, `::TestProtocolVersionAttribute`. +`propagation_only` is applied at the seam span, which is where the incoming `_meta` parent context is established for the whole request; suppressing only the deeper `server_span` would leave the per-request SERVER span intact and defeat the mode. + +*Verify:* `fastmcp_slim/fastmcp/settings.py` (`telemetry_mode`); `fastmcp_slim/fastmcp/telemetry.py` (`telemetry_mode`, `get_tracer`, `suppress_fastmcp_telemetry`); `fastmcp_slim/fastmcp/server/telemetry.py` (`_propagation_only_span`, `seam_span`, `get_protocol_span_attributes`); `tests/server/telemetry/test_server_tracing.py::TestTelemetryEnabledByDefault`, `::TestProtocolVersionAttribute`; `tests/telemetry/test_interop.py`. ### Spec-correct error codes via a central translator — Breaking (wire error code) diff --git a/dev-docs/v4-notes/feature-program.md b/dev-docs/v4-notes/feature-program.md index 4e185d6dd..8c7b9f022 100644 --- a/dev-docs/v4-notes/feature-program.md +++ b/dev-docs/v4-notes/feature-program.md @@ -103,7 +103,7 @@ This workstream also owns the server-side statelessness design holes — `ctx.se A cluster of protocol features tracked for v4. Their statuses have diverged: - **Cache hints — shipped (#4464).** Server-level authoring (`FastMCP(cache_ttl=..., cache_scope=...)`, SEP-2549) stamps every cacheable result, and the FastMCP client honors hints with an opt-in response cache. -- **OpenTelemetry — shipped (#4481).** Spans are on by default (a no-op without an exporter), with SDK-aligned attributes and a `FASTMCP_ENABLE_TELEMETRY=false` off-switch. +- **OpenTelemetry — shipped (#4481).** Spans are on by default (a no-op without an exporter), with SDK-aligned attributes and a `FASTMCP_TELEMETRY_MODE` setting (`native` / `propagation_only` / `off`). - **Extensions — client side shipped (#4572).** `Client(extensions=..., result_claims=...)` advertises opt-in client extensions (SEP-2133). The server side is a Designed workstream in its own right (see [FastMCP-native extension API](#fastmcp-native-extension-api)). The cross-era reconciliation of the `extensions` / MCP Apps capability advertisement is still open (the capability is stripped at pre-2026 negotiated versions — sdk-feedback #2). - **Subscriptions — not started.** A `subscriptions/listen` surface backed by a subscription bus. diff --git a/dev-docs/v4-notes/protocol-2026.md b/dev-docs/v4-notes/protocol-2026.md index 1c77683fc..fb3ef5428 100644 --- a/dev-docs/v4-notes/protocol-2026.md +++ b/dev-docs/v4-notes/protocol-2026.md @@ -45,7 +45,7 @@ The complete picture of what a FastMCP v4 server and client provide on the `2026 | **Middleware** | Typed per-method hooks (`on_call_tool`, `on_list_tools`, …) and a suite of built-ins (auth, rate limiting, caching, error handling, logging, timing, and more). | | **Composition** | `mount()`, providers, proxying, and tool transforms compose servers dynamically at runtime, with lifespans and middleware driven through the SDK session manager. | | **Pagination** | Declarative `FastMCP(list_page_size=...)` paginates all list operations in the high-level server; the client auto-paginates with cycle detection. | -| **Telemetry** | OpenTelemetry spans on by default (no-op without an exporter), SDK-aligned attributes (`mcp.method.name`, `mcp.protocol.version`, `gen_ai.*`), plus auth and provider-delegation spans; `FASTMCP_ENABLE_TELEMETRY=false` disables cleanly. | +| **Telemetry** | OpenTelemetry spans on by default (no-op without an exporter), SDK-aligned attributes (`mcp.method.name`, `mcp.protocol.version`, `gen_ai.*`), plus auth and provider-delegation spans; `FASTMCP_TELEMETRY_MODE` selects `native`, `propagation_only` (interop with an outer MCP instrumentation layer), or `off`. | | **Background tasks (SEP-2663)** | `fastmcp-tasks` implements the `io.modelcontextprotocol/tasks` extension end to end: `mcp.add_extension(TasksExtension())` plus `task=True` runs a tool as a background task, driven by the same Docket engine FastMCP 3 used. A client transparently completes a tasked call; gathering input mid-task uses the same guard pattern as foreground multi-round-trip tools, so a tool is written once and works either way. Modern-protocol only — the `task=True` runtime this replaced (SEP-1686) is gone entirely, not bridged. See [Background Tasks (SEP-2663)](background-tasks.md) for the design and [servers/tasks](https://gofastmcp.com/servers/tasks) for usage. | ## Still in the program diff --git a/docs/more/settings.mdx b/docs/more/settings.mdx index 0fa4ca0e2..6ea808af7 100644 --- a/docs/more/settings.mdx +++ b/docs/more/settings.mdx @@ -77,7 +77,7 @@ These control how the server listens when running with an HTTP transport. | Environment Variable | Type | Default | Description | |---|---|---|---| -| `FASTMCP_ENABLE_TELEMETRY` | `bool` | `true` | Whether FastMCP's native [OpenTelemetry instrumentation](/servers/telemetry) is active. Enabled by default; FastMCP uses only the OpenTelemetry API, so span creation is a no-op with negligible overhead unless an OpenTelemetry SDK and exporter are configured. Set to `false` to turn instrumentation off entirely, in which case no FastMCP spans are created even when an SDK is configured. | +| `FASTMCP_TELEMETRY_MODE` | `Literal["native", "propagation_only", "off"]` | `native` | Controls FastMCP's native [OpenTelemetry instrumentation](/servers/telemetry). `native` emits FastMCP's MCP spans and propagates trace context; because FastMCP uses only the OpenTelemetry API, this costs almost nothing unless an SDK and exporter are configured. `propagation_only` keeps `_meta` trace propagation and still parents downstream spans from the incoming context, but emits none of FastMCP's own spans, so another instrumentation layer can own the MCP span hierarchy. `off` is a full pass-through: no spans, and no trace context extracted or attached. | ## Tasks (Docket) diff --git a/docs/servers/telemetry.mdx b/docs/servers/telemetry.mdx index 2d3a96c30..9573f07bd 100644 --- a/docs/servers/telemetry.mdx +++ b/docs/servers/telemetry.mdx @@ -21,11 +21,21 @@ FastMCP uses the OpenTelemetry API for instrumentation. This means: Because FastMCP only depends on the OpenTelemetry API, span creation is a no-op until you configure an SDK and exporter — so being on by default costs nothing until you opt into collection. -### Turning Telemetry Off +### Telemetry Modes -To disable FastMCP's instrumentation entirely, set `FASTMCP_ENABLE_TELEMETRY=false` (or `fastmcp.settings.enable_telemetry = False`). When disabled, FastMCP creates no spans even if an SDK is configured. +`FASTMCP_TELEMETRY_MODE` (or `fastmcp.settings.telemetry_mode`) controls how much of the instrumentation is active: + +| Mode | FastMCP spans | Trace context | +|---|---|---| +| `native` (default) | Emitted | Propagated | +| `propagation_only` | Suppressed | Propagated | +| `off` | Suppressed | Untouched | + +Use `off` to disable FastMCP's instrumentation entirely. No spans are created even if an SDK is configured, and FastMCP leaves the surrounding OpenTelemetry context exactly as it found it. + +Use `propagation_only` when another instrumentation layer already owns the MCP span hierarchy — see [Interoperability](#interoperability) below. ## Enabling Telemetry @@ -148,6 +158,37 @@ trace.set_tracer_provider(provider) The name check must happen before `ParentBased` delegates. If the name-based sampler is nested inside `ParentBased`, it is not consulted for child spans whose parent was already sampled. +## Interoperability + + + +FastMCP assumes it owns the MCP span hierarchy. When something else already owns it — an MCP-aware OpenTelemetry instrumentation library, or a service mesh that understands the protocol — FastMCP's spans duplicate what that layer already emits, and the same request shows up twice in your traces. + +Setting `propagation_only` resolves the duplication in FastMCP's favor of the other layer: + +```bash +export FASTMCP_TELEMETRY_MODE=propagation_only +``` + +The distinction from `off` matters here. Both emit no FastMCP spans, but `off` is fully transparent, while `propagation_only` still extracts the trace context arriving in `_meta` and attaches it for the duration of the request. Spans created downstream — by your tool handlers, or by the instrumentation layer that owns the hierarchy — are parented to the calling trace rather than starting a new one. Outbound requests still carry `traceparent` and `tracestate` in `_meta`. + +### Suppressing spans for a single block + +Library authors embedding FastMCP inside their own instrumented stack often want to own the hierarchy for one specific operation rather than process-wide. `suppress_fastmcp_telemetry()` applies `propagation_only` semantics to a block: + +```python +from fastmcp import Client +from fastmcp.telemetry import suppress_fastmcp_telemetry + +async def search(client: Client, query: str): + with suppress_fastmcp_telemetry(): + return await client.call_tool("search", {"query": query}) +``` + +This is narrower than OpenTelemetry's global instrumentation suppression: only FastMCP's spans are skipped, so nested instrumentation for HTTP clients, databases, and everything else keeps emitting normally. + +The context manager has no effect when `telemetry_mode` is already `off`. A request to skip FastMCP's spans cannot re-enable the context propagation that `off` deliberately omits. + ## Programmatic Configuration For more control, configure the SDK in your Python code before importing FastMCP: diff --git a/fastmcp_slim/fastmcp/server/telemetry.py b/fastmcp_slim/fastmcp/server/telemetry.py index 53350e8e8..4d073d45b 100644 --- a/fastmcp_slim/fastmcp/server/telemetry.py +++ b/fastmcp_slim/fastmcp/server/telemetry.py @@ -4,14 +4,23 @@ from collections.abc import Generator from contextlib import contextmanager from contextvars import ContextVar +from opentelemetry import context as otel_context from opentelemetry.context import Context -from opentelemetry.trace import Span, SpanKind, Status, StatusCode, get_current_span +from opentelemetry.trace import ( + INVALID_SPAN, + Span, + SpanKind, + Status, + StatusCode, + get_current_span, +) from fastmcp.exceptions import ToolError as _ToolError from fastmcp.telemetry import ( extract_trace_context, get_tracer, restore_dropped_attributes, + telemetry_mode, ) # Marker attribute set on the SERVER span opened at the FastMCP middleware seam @@ -87,6 +96,32 @@ def _get_parent_trace_context() -> Context | None: return None +@contextmanager +def _propagation_only_span() -> Generator[Span, None, None]: + """Attach the incoming `_meta` trace context without creating a span. + + This is what separates `propagation_only` from `off`. Both create no + FastMCP spans, but `off` is fully transparent while `propagation_only` + still has to *parent* whatever the request goes on to do: without the + attach here, the trace context carried in `_meta` would be extracted and + then thrown away, and a span created inside a tool handler — by the user or + by the outer instrumentation layer that owns the MCP hierarchy — would + start a brand new trace instead of continuing the caller's. + + Yields `INVALID_SPAN`, which is non-recording, so callers' `is_recording()` + guards skip attribute and error bookkeeping on it. + """ + parent_context = _get_parent_trace_context() + if parent_context is None: + yield INVALID_SPAN + return + token = otel_context.attach(parent_context) + try: + yield INVALID_SPAN + finally: + otel_context.detach(token) + + def _build_server_span_attrs( method: str, server_name: str, @@ -138,7 +173,16 @@ def seam_span(method: str, server_name: str) -> Generator[Span, None, None]: opening a second one. Exceptions raised anywhere below the seam — including rejections *before* the high-level path (auth, not-found, middleware vetoes) that would otherwise produce no SERVER span at all — are recorded here. + + In `propagation_only` mode no span is opened at all — this is the one place + that has to know the difference, because the seam is where the incoming + `_meta` parent context is applied for the whole request. """ + if telemetry_mode() == "propagation_only": + with _propagation_only_span() as span: + yield span + return + attrs = { SEAM_SPAN_MARKER: True, "mcp.method.name": method, @@ -198,7 +242,17 @@ def server_span( new SERVER span as before. Automatically records any exception on the span and sets error status. + + In `propagation_only` mode no span is opened or enriched. The seam has + normally already attached the incoming parent context for this request; + doing it again here is a no-op, and covers the in-process callers that + bypass the dispatcher and so never reach the seam at all. """ + if telemetry_mode() == "propagation_only": + with _propagation_only_span() as span: + yield span + return + attrs = _build_server_span_attrs( method, server_name, diff --git a/fastmcp_slim/fastmcp/settings.py b/fastmcp_slim/fastmcp/settings.py index 352b383e2..d442d7218 100644 --- a/fastmcp_slim/fastmcp/settings.py +++ b/fastmcp_slim/fastmcp/settings.py @@ -20,6 +20,8 @@ ENV_FILE = os.getenv("FASTMCP_ENV_FILE", ".env") LOG_LEVEL = Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] +TELEMETRY_MODE = Literal["native", "propagation_only", "off"] + MCP_LOG_LEVEL = Literal[ "debug", "info", "notice", "warning", "error", "critical", "alert", "emergency" ] @@ -104,24 +106,6 @@ class Settings(BaseSettings): ), ] = True - enable_telemetry: Annotated[ - bool, - Field( - description=inspect.cleandoc( - """ - Whether FastMCP's native OpenTelemetry instrumentation is active. - Enabled by default: FastMCP uses only the OpenTelemetry API, so - span creation is a no-op with negligible overhead unless an - OpenTelemetry SDK and exporter are configured. Set to False to - turn instrumentation off entirely, in which case FastMCP's span - helpers become a transparent pass-through: no FastMCP spans are - created even when an SDK is configured, and the surrounding OTel - trace context is left untouched. - """ - ) - ), - ] = True - deprecation_warnings: Annotated[ bool, Field( @@ -167,6 +151,31 @@ class Settings(BaseSettings): ), ] = True + telemetry_mode: Annotated[ + TELEMETRY_MODE, + Field( + description=inspect.cleandoc( + """ + Controls FastMCP's native OpenTelemetry instrumentation. + + - `native` (default): FastMCP creates MCP spans and propagates + trace context through request `_meta`. FastMCP uses only the + OpenTelemetry API, so span creation is a no-op with negligible + overhead unless an SDK and exporter are configured. + - `propagation_only`: FastMCP still injects and extracts trace + context, and still parents downstream spans from the incoming + `_meta` context, but creates none of its own MCP spans. Use + this when another instrumentation layer owns the MCP span + hierarchy and FastMCP's spans would duplicate it. + - `off`: FastMCP's span helpers become a transparent + pass-through. No spans are created even when an SDK is + configured, and the surrounding OTel context is left + untouched — no trace context is extracted or attached. + """ + ), + ), + ] = "native" + client_init_timeout: Annotated[ float | None, Field( diff --git a/fastmcp_slim/fastmcp/telemetry.py b/fastmcp_slim/fastmcp/telemetry.py index dc0edf32a..cf379818b 100644 --- a/fastmcp_slim/fastmcp/telemetry.py +++ b/fastmcp_slim/fastmcp/telemetry.py @@ -23,7 +23,7 @@ Example usage with SDK: from collections.abc import Iterator, Mapping from contextlib import contextmanager -from typing import Any, Protocol, runtime_checkable +from typing import TYPE_CHECKING, Any, Protocol, runtime_checkable from opentelemetry import context as otel_context from opentelemetry import propagate, trace @@ -40,6 +40,9 @@ from opentelemetry.trace import ( from opentelemetry.trace import get_tracer as otel_get_tracer from opentelemetry.util import types as otel_types +if TYPE_CHECKING: + from fastmcp.settings import TELEMETRY_MODE as TelemetryMode + INSTRUMENTATION_NAME = "fastmcp" TRACE_PARENT_KEY = "traceparent" @@ -77,28 +80,71 @@ class _DisabledTracer(NoOpTracer): _DISABLED_TRACER = _DisabledTracer() +_SUPPRESS_KEY = otel_context.create_key("fastmcp_suppress_telemetry") + + +def telemetry_mode() -> "TelemetryMode": + """Resolve the effective telemetry mode for the current context. + + This is `fastmcp.settings.telemetry_mode`, except that an active + `suppress_fastmcp_telemetry()` block downgrades `native` to + `propagation_only`. Suppression never upgrades or overrides `off`: `off` + means FastMCP touches nothing, and a narrower request to skip FastMCP's + spans cannot re-enable the context propagation `off` deliberately omits. + """ + import fastmcp + + mode: TelemetryMode = fastmcp.settings.telemetry_mode + if mode == "native" and otel_context.get_value(_SUPPRESS_KEY): + return "propagation_only" + return mode + + +def native_spans_enabled() -> bool: + """Whether FastMCP should create its own spans right now.""" + return telemetry_mode() == "native" + + +@contextmanager +def suppress_fastmcp_telemetry() -> Iterator[None]: + """Suppress FastMCP's own spans without disabling trace propagation. + + Scoped equivalent of `telemetry_mode="propagation_only"`, for callers that + embed FastMCP inside their own instrumented stack and want to own the MCP + span hierarchy for a specific block. Narrower than OpenTelemetry's global + instrumentation suppression: only FastMCP's spans are skipped, so nested + instrumentation (HTTP clients, databases) keeps emitting, and trace context + still flows through `_meta` so those spans are parented correctly. + + Has no effect when `telemetry_mode` is already `off`. + """ + token = otel_context.attach(otel_context.set_value(_SUPPRESS_KEY, True)) + try: + yield + finally: + otel_context.detach(token) + def get_tracer(version: str | None = None) -> Tracer: """Get the FastMCP tracer for creating spans. Instrumentation is on by default. FastMCP uses only the OpenTelemetry API, so span creation is a no-op with negligible overhead unless an OpenTelemetry - SDK and exporter are configured. Set `fastmcp.settings.enable_telemetry` to - False (env `FASTMCP_ENABLE_TELEMETRY=false`) to turn instrumentation off - entirely, in which case this returns a pass-through tracer that leaves the - current OTel context untouched even when an SDK is configured. + SDK and exporter are configured. When `fastmcp.settings.telemetry_mode` is + `propagation_only` or `off` — or the caller is inside a + `suppress_fastmcp_telemetry()` block — this returns a pass-through tracer + that creates no spans and leaves the current OTel context untouched even + when an SDK is configured. Args: version: Optional version string for the instrumentation Returns: - A tracer instance. Returns a non-attaching pass-through tracer if - telemetry is disabled; span creation is otherwise a no-op unless an SDK - is configured. + A tracer instance. Returns a non-attaching pass-through tracer when + FastMCP's own spans are disabled; span creation is otherwise a no-op + unless an SDK is configured. """ - import fastmcp - - if not fastmcp.settings.enable_telemetry: + if not native_spans_enabled(): return _DISABLED_TRACER return otel_get_tracer(INSTRUMENTATION_NAME, version) @@ -115,6 +161,11 @@ def inject_trace_context( A new dict containing the original meta (if any) plus trace context keys, or None if no trace context to inject and meta was None """ + # `off` means FastMCP touches nothing, outbound propagation included. + # `propagation_only` still injects — carrying context is the whole point. + if telemetry_mode() == "off": + return meta + carrier: dict[str, str] = {} propagate.inject(carrier) @@ -222,6 +273,10 @@ def extract_trace_context(meta: dict[str, Any] | None) -> Context: An OpenTelemetry Context with the extracted trace context, or the current context if no trace context found or already in a trace """ + # `off` means FastMCP touches nothing, including the surrounding context. + if telemetry_mode() == "off": + return otel_context.get_current() + # Don't override existing trace context (e.g., from HTTP propagation) current_span = trace.get_current_span() if current_span.get_span_context().is_valid: @@ -237,7 +292,12 @@ def extract_trace_context(meta: dict[str, Any] | None) -> Context: carrier["tracestate"] = str(meta[TRACE_STATE_KEY]) if carrier: - return propagate.extract(carrier) + # Extract *onto the current context* rather than a fresh root, so the + # incoming parent is added without discarding context values the + # caller already established — active baggage, and FastMCP's own + # suppression marker, which would otherwise be dropped the moment the + # extracted context is attached. + return propagate.extract(carrier, context=otel_context.get_current()) return otel_context.get_current() @@ -248,6 +308,9 @@ __all__ = [ "extract_trace_context", "get_tracer", "inject_trace_context", + "native_spans_enabled", "record_span_error", "restore_dropped_attributes", + "suppress_fastmcp_telemetry", + "telemetry_mode", ] diff --git a/tests/server/telemetry/test_server_tracing.py b/tests/server/telemetry/test_server_tracing.py index 3454d93ab..252d92b70 100644 --- a/tests/server/telemetry/test_server_tracing.py +++ b/tests/server/telemetry/test_server_tracing.py @@ -587,14 +587,14 @@ class TestTelemetryEnabledByDefault: """Instrumentation is on by default and controllable via the off-switch. FastMCP uses only the OpenTelemetry API, so spans are created unconditionally - and light up when an SDK is configured. `FASTMCP_ENABLE_TELEMETRY=false` - (`fastmcp.settings.enable_telemetry`) turns span creation off entirely, so no + and light up when an SDK is configured. `FASTMCP_TELEMETRY_MODE=off` + (`fastmcp.settings.telemetry_mode`) turns span creation off entirely, so no FastMCP spans are exported even with an SDK configured. """ async def test_spans_fire_by_default(self, trace_exporter: InMemorySpanExporter): """No opt-in required: a tool call produces a span out of the box.""" - assert fastmcp.settings.enable_telemetry is True + assert fastmcp.settings.telemetry_mode == "native" mcp = FastMCP("test-server") @@ -615,7 +615,7 @@ class TestTelemetryEnabledByDefault: ): """With telemetry disabled, no spans are created even with an SDK configured (the exporter fixture installs one).""" - monkeypatch.setattr(fastmcp.settings, "enable_telemetry", False) + monkeypatch.setattr(fastmcp.settings, "telemetry_mode", "off") mcp = FastMCP("test-server") @@ -641,7 +641,7 @@ class TestTelemetryEnabledByDefault: are governed by the user's OpenTelemetry SDK, not FastMCP's off-switch, so they may still appear — the assertion filters them out. """ - monkeypatch.setattr(fastmcp.settings, "enable_telemetry", False) + monkeypatch.setattr(fastmcp.settings, "telemetry_mode", "off") mcp = FastMCP("test-server") @@ -676,7 +676,7 @@ class TestTelemetryEnabledByDefault: `trace.get_current_span()` inside a handler must still return the caller's enclosing span, and attributes written there must land on it. """ - monkeypatch.setattr(fastmcp.settings, "enable_telemetry", False) + monkeypatch.setattr(fastmcp.settings, "telemetry_mode", "off") tracer = trace.get_tracer("test-enclosing") captured: dict[str, Span] = {} diff --git a/tests/telemetry/test_interop.py b/tests/telemetry/test_interop.py new file mode 100644 index 000000000..5befb5811 --- /dev/null +++ b/tests/telemetry/test_interop.py @@ -0,0 +1,280 @@ +"""Tests for telemetry interoperability modes. + +Validates that FastMCP's own spans can be suppressed — globally via +`telemetry_mode` or per-block via `suppress_fastmcp_telemetry()` — while trace +context propagation keeps working in `propagation_only` mode and is fully +disabled in `off` mode. +""" + +from __future__ import annotations + +import pytest +from opentelemetry import context as otel_context +from opentelemetry import trace as otel_trace +from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter +from opentelemetry.trace import INVALID_SPAN, SpanKind + +import fastmcp +from fastmcp import Client, Context, FastMCP +from fastmcp.client.telemetry import client_span +from fastmcp.server.telemetry import delegate_span, server_span +from fastmcp.telemetry import ( + extract_trace_context, + inject_trace_context, + native_spans_enabled, + suppress_fastmcp_telemetry, + telemetry_mode, +) + +# A well-formed W3C traceparent for extraction tests. +TRACEPARENT = "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01" + + +@pytest.fixture +def mode(monkeypatch: pytest.MonkeyPatch): + """Set `fastmcp.settings.telemetry_mode` for the duration of a test.""" + + def _set(value: str) -> None: + monkeypatch.setattr(fastmcp.settings, "telemetry_mode", value) + + return _set + + +def fastmcp_spans(exporter: InMemorySpanExporter) -> list[str]: + """Names of spans emitted by FastMCP's own instrumentation scope.""" + return [ + s.name + for s in exporter.get_finished_spans() + if s.instrumentation_scope is not None + and s.instrumentation_scope.name == "fastmcp" + ] + + +class TestTelemetryModeResolution: + def test_native_by_default(self): + assert telemetry_mode() == "native" + assert native_spans_enabled() + + @pytest.mark.parametrize("value", ["propagation_only", "off"]) + def test_setting_disables_native_spans(self, value: str, mode): + mode(value) + assert telemetry_mode() == value + assert not native_spans_enabled() + + def test_suppress_downgrades_native_to_propagation_only(self): + with suppress_fastmcp_telemetry(): + assert telemetry_mode() == "propagation_only" + assert not native_spans_enabled() + assert telemetry_mode() == "native" + + def test_suppress_cannot_override_off(self, mode): + """`off` means FastMCP touches nothing. A narrower request to skip + FastMCP's spans must not re-enable the propagation `off` omits.""" + mode("off") + with suppress_fastmcp_telemetry(): + assert telemetry_mode() == "off" + + def test_suppress_nests(self): + with suppress_fastmcp_telemetry(): + with suppress_fastmcp_telemetry(): + assert not native_spans_enabled() + # Outer suppression still active after the inner block exits. + assert not native_spans_enabled() + assert native_spans_enabled() + + def test_suppress_restores_on_exception(self): + with pytest.raises(RuntimeError): + with suppress_fastmcp_telemetry(): + raise RuntimeError("boom") + assert native_spans_enabled() + + +class TestSpanHelperSuppression: + """Every FastMCP span helper goes quiet when its own spans are disabled.""" + + @pytest.fixture + def helpers(self): + return { + "server": lambda: server_span( + name="test_op", + method="tools/call", + server_name="test-server", + component_type="tool", + component_key="tool://test", + ), + "client": lambda: client_span( + name="test_client", + method="tools/call", + component_key="tool://test", + ), + "delegate": lambda: delegate_span( + name="test_delegate", + provider_type="FastMCPProvider", + component_key="tool://test", + ), + } + + @pytest.mark.parametrize("helper", ["server", "client", "delegate"]) + @pytest.mark.parametrize("value", ["propagation_only", "off"]) + def test_helper_emits_nothing( + self, + helper: str, + value: str, + helpers, + mode, + trace_exporter: InMemorySpanExporter, + ): + mode(value) + with helpers[helper]() as span: + assert span is INVALID_SPAN + assert trace_exporter.get_finished_spans() == () + + @pytest.mark.parametrize("helper", ["server", "client", "delegate"]) + def test_helper_emits_nothing_under_suppress( + self, helper: str, helpers, trace_exporter: InMemorySpanExporter + ): + with suppress_fastmcp_telemetry(): + with helpers[helper]() as span: + assert span is INVALID_SPAN + assert trace_exporter.get_finished_spans() == () + + @pytest.mark.parametrize("helper", ["server", "client", "delegate"]) + def test_helper_emits_by_default( + self, helper: str, helpers, trace_exporter: InMemorySpanExporter + ): + with helpers[helper](): + pass + assert len(trace_exporter.get_finished_spans()) == 1 + + +class TestContextPropagation: + """`propagation_only` keeps trace context flowing; `off` does not.""" + + def test_extract_preserves_current_context_values(self): + """Regression: extracting the incoming traceparent must not discard + context values the caller already established. Extracting onto a fresh + root would drop FastMCP's own suppression marker (and any baggage), so + attaching the result would silently re-enable FastMCP's spans. + """ + with suppress_fastmcp_telemetry(): + parent = extract_trace_context({"traceparent": TRACEPARENT}) + token = otel_context.attach(parent) + try: + assert telemetry_mode() == "propagation_only" + finally: + otel_context.detach(token) + + def test_extract_applies_incoming_parent(self, mode): + mode("propagation_only") + parent = extract_trace_context({"traceparent": TRACEPARENT}) + token = otel_context.attach(parent) + try: + span_context = otel_trace.get_current_span().get_span_context() + assert format(span_context.trace_id, "032x") == ( + "4bf92f3577b34da6a3ce929d0e0e4736" + ) + finally: + otel_context.detach(token) + + def test_off_ignores_incoming_parent(self, mode): + """`off` is a full pass-through: the incoming context is not applied.""" + mode("off") + parent = extract_trace_context({"traceparent": TRACEPARENT}) + assert parent is otel_context.get_current() + + def test_off_does_not_inject(self, mode, trace_exporter: InMemorySpanExporter): + mode("off") + with otel_trace.get_tracer("test").start_as_current_span("root"): + assert inject_trace_context({"existing": 1}) == {"existing": 1} + + def test_propagation_only_still_injects( + self, mode, trace_exporter: InMemorySpanExporter + ): + mode("propagation_only") + with otel_trace.get_tracer("test").start_as_current_span("root"): + meta = inject_trace_context() + assert meta is not None and "traceparent" in meta + + +class TestEndToEnd: + """A real in-process client drives a real server — nothing monkeypatched + beyond the setting itself.""" + + async def test_propagation_only_parents_downstream_user_spans( + self, mode, trace_exporter: InMemorySpanExporter + ): + mode("propagation_only") + captured: dict[str, int] = {} + + server = FastMCP("interop-server") + + @server.tool + async def work(ctx: Context) -> str: + # A span the *user* creates inside their handler. + tracer = otel_trace.get_tracer("user-code") + with tracer.start_as_current_span("user-span") as span: + captured["downstream"] = span.get_span_context().trace_id + return "done" + + async with Client(server) as client: + tracer = otel_trace.get_tracer("client-code") + with tracer.start_as_current_span("client-root") as root: + captured["client"] = root.get_span_context().trace_id + await client.call_tool("work", {}) + + names = [s.name for s in trace_exporter.get_finished_spans()] + assert "user-span" in names and "client-root" in names + # FastMCP emitted none of its own spans — including the per-request + # SERVER span opened at the middleware seam, which is the whole point. + assert fastmcp_spans(trace_exporter) == [] + assert [ + s for s in trace_exporter.get_finished_spans() if s.kind == SpanKind.SERVER + ] == [] + # ...yet the user's span inherited the incoming distributed trace. + assert captured["client"] == captured["downstream"] + + async def test_propagation_only_without_incoming_trace( + self, mode, trace_exporter: InMemorySpanExporter + ): + """With no surrounding client span there is no incoming trace. The call + must still succeed and emit no FastMCP spans.""" + mode("propagation_only") + captured: dict[str, int] = {} + + server = FastMCP("interop-server") + + @server.tool + async def work(ctx: Context) -> str: + tracer = otel_trace.get_tracer("user-code") + with tracer.start_as_current_span("user-span") as span: + captured["downstream"] = span.get_span_context().trace_id + return "done" + + async with Client(server) as client: + result = await client.call_tool("work", {}) + + assert result.data == "done" + assert fastmcp_spans(trace_exporter) == [] + # A self-rooted trace was created (no incoming parent to inherit). + assert "downstream" in captured + + async def test_suppress_block_silences_a_single_call( + self, trace_exporter: InMemorySpanExporter + ): + """The scoped form suppresses one call and leaves the next instrumented.""" + server = FastMCP("interop-server") + + @server.tool + async def work() -> str: + return "done" + + async with Client(server) as client: + # Drop the spans the connection handshake already emitted. + trace_exporter.clear() + + with suppress_fastmcp_telemetry(): + await client.call_tool("work", {}) + assert fastmcp_spans(trace_exporter) == [] + + await client.call_tool("work", {}) + assert fastmcp_spans(trace_exporter) != [] From 7674645761bc822facf12fafd2932392ce6548f7 Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Mon, 27 Jul 2026 16:06:54 -0400 Subject: [PATCH 05/53] Flatten OpenAPI discriminator subtypes into request bodies (#4677) * Flatten OpenAPI discriminator subtypes into request bodies * Resolve schema-name discriminator mappings and union conflicting variant fields * Advertise discriminator values for propertyless variants and document the behavior --- docs/integrations/openapi.mdx | 19 +- .../fastmcp/utilities/openapi/parser.py | 42 ++- .../fastmcp/utilities/openapi/schemas.py | 123 ++++++++ .../openapi/test_openapi_discriminator.py | 286 ++++++++++++++++++ 4 files changed, 459 insertions(+), 11 deletions(-) create mode 100644 tests/server/providers/openapi/test_openapi_discriminator.py diff --git a/docs/integrations/openapi.mdx b/docs/integrations/openapi.mdx index f6fb1a13f..4769f6576 100644 --- a/docs/integrations/openapi.mdx +++ b/docs/integrations/openapi.mdx @@ -452,4 +452,21 @@ FastMCP handles array parameters according to OpenAPI specifications: ### Headers -Header parameters are automatically converted to strings and included in the HTTP request. \ No newline at end of file +Header parameters are automatically converted to strings and included in the HTTP request. + +### Composed Request Bodies + +A request body becomes a flat set of tool arguments, which is the shape LLM tool-calling APIs fill in most reliably. Schemas composed with `allOf` are resolved first, following `$ref` members, so fields inherited from a parent schema appear alongside the ones a schema declares itself. + +Schemas that use a `discriminator` are flattened the same way. FastMCP merges in the fields of every subtype named in the discriminator's `mapping`, marks them optional, and names the accepted values on the discriminator's own description. Given a `Pet` body discriminated by `petType` and mapped onto `Cat` and `Dog`, the tool takes the discriminator plus whichever fields that variant uses: + +```python +await client.call_tool("create_pet", { + "petType": "cat", + "meowVolume": 11, +}) +``` + +The discriminator stays required; every variant field is optional, because only one variant applies to any given call. + +This trades local strictness for a schema models complete accurately. The generated schema permits any combination of variant fields, so sending `packSize` with `petType: "cat"` passes FastMCP's validation and is rejected by the API itself, exactly as it would be for any other HTTP client. Where two variants declare the same field differently, the declarations are combined with `anyOf` so that neither variant's constraints are advertised as applying to both. \ No newline at end of file diff --git a/fastmcp_slim/fastmcp/utilities/openapi/parser.py b/fastmcp_slim/fastmcp/utilities/openapi/parser.py index 7ad94a93d..e492a4c5a 100644 --- a/fastmcp_slim/fastmcp/utilities/openapi/parser.py +++ b/fastmcp_slim/fastmcp/utilities/openapi/parser.py @@ -36,6 +36,7 @@ from .models import ( ) from .schemas import ( _combine_schemas_and_map_params, + _discriminator_target_name, _replace_ref_with_defs, ) @@ -543,6 +544,7 @@ class OpenAPIParser( schema: dict, all_schemas: dict[str, Any], collected: set[str] | None = None, + follow_discriminator: bool = False, ) -> set[str]: """ Extract all schema names referenced by a schema (including transitive dependencies). @@ -551,6 +553,10 @@ class OpenAPIParser( schema: The schema to analyze all_schemas: All available schema definitions collected: Set of already collected schema names (for recursion) + follow_discriminator: Also collect the subtypes named by a + `discriminator.mapping`. Those values are bare strings rather + than `$ref` objects, so they are invisible to ordinary ref + collection. Returns: Set of schema names that are referenced @@ -558,6 +564,12 @@ class OpenAPIParser( if collected is None: collected = set() + def collect(schema_name: str) -> None: + """Collect a schema by name and recurse into its dependencies.""" + if schema_name not in collected and schema_name in all_schemas: + collected.add(schema_name) + find_refs(all_schemas[schema_name]) + def find_refs(obj): """Recursively find all $ref references.""" if isinstance(obj, dict): @@ -570,14 +582,19 @@ class OpenAPIParser( return # Add this schema and recursively find its dependencies - if ( - collected is not None - and schema_name not in collected - and schema_name in all_schemas - ): - collected.add(schema_name) - # Recursively find dependencies of this schema - find_refs(all_schemas[schema_name]) + collect(schema_name) + + if follow_discriminator: + discriminator = obj.get("discriminator") + if isinstance(discriminator, dict): + mapping = discriminator.get("mapping") + if isinstance(mapping, dict): + for target in mapping.values(): + if not isinstance(target, str): + continue + name = _discriminator_target_name(target) + if name: + collect(name) # Continue searching in all values for value in obj.values(): @@ -614,10 +631,15 @@ class OpenAPIParser( deps = self._extract_schema_dependencies(param.schema_, all_schemas) needed_schemas.update(deps) - # Check request body for schema references + # Check request body for schema references. Request bodies are flattened + # into a single object, so discriminated subtypes need to come along. if request_body and request_body.content_schema: for content_schema in request_body.content_schema.values(): - deps = self._extract_schema_dependencies(content_schema, all_schemas) + deps = self._extract_schema_dependencies( + content_schema, + all_schemas, + follow_discriminator=True, + ) needed_schemas.update(deps) # Return only the needed input schemas diff --git a/fastmcp_slim/fastmcp/utilities/openapi/schemas.py b/fastmcp_slim/fastmcp/utilities/openapi/schemas.py index f272111b3..c8fe95b9b 100644 --- a/fastmcp_slim/fastmcp/utilities/openapi/schemas.py +++ b/fastmcp_slim/fastmcp/utilities/openapi/schemas.py @@ -258,6 +258,118 @@ def _allof_members( return [schema] +def _discriminator_target_name(target: str) -> str | None: + """Resolve a ``discriminator.mapping`` value to a local schema name. + + Mapping values hold "schema names or references", so a bare ``"Cat"`` means + the ``Cat`` component just as ``"#/components/schemas/Cat"`` does. Anything + else — a remote URL, a pointer outside the component schemas — has no local + definition to flatten. + """ + for prefix in ("#/$defs/", "#/components/schemas/"): + if target.startswith(prefix): + return target.removeprefix(prefix) or None + if target.startswith("#") or "/" in target: + return None + return target or None + + +def _flatten_discriminator_subtypes( + schema: dict[str, Any], + schema_defs: dict[str, Any], +) -> dict[str, Any] | None: + """Flatten the subtypes named by an OpenAPI ``discriminator.mapping``. + + A parent schema carrying a discriminator describes its children only + through ``mapping``, so the child fields are unreachable from the parent's + own ``properties``. Rather than emitting a branch per subtype, the fields + are merged in as optional and the variants are spelled out on the + discriminator property's description. Top-level ``oneOf`` is filled in + poorly by LLM tool-calling APIs, and the upstream API remains the real + validator either way: a field from the wrong variant is rejected there + rather than locally. + + Only the mapping on *schema* itself is expanded. A subtype carrying its own + discriminator is left alone, which also keeps the parent/child reference + cycle from recursing. + + Returns replacement ``properties`` for *schema*, or None when there is no + usable discriminator mapping to flatten. + """ + discriminator = schema.get("discriminator") + if not isinstance(discriminator, dict): + return None + + property_name = discriminator.get("propertyName") + mapping = discriminator.get("mapping") + if not isinstance(property_name, str) or not isinstance(mapping, dict): + return None + + own_props = schema.get("properties", {}) + # Variants that disagree about a property are unioned rather than resolved: + # keeping whichever came first would advertise one variant's constraint + # (a `const` tag, say) while claiming to accept all of them. + alternatives: dict[str, list[Any]] = {} + values: list[str] = [] + variants: list[str] = [] + + for value, target in mapping.items(): + if not isinstance(target, str): + continue + + name = _discriminator_target_name(target) + subtype = schema_defs.get(name) if name else None + if not isinstance(subtype, dict): + continue + + # Fields the parent already declares are shared, not variant-specific. + variant_fields: list[str] = [] + for member in _allof_members(subtype, schema_defs): + for prop_name, prop_schema in member.get("properties", {}).items(): + if prop_name in own_props: + continue + if prop_name not in variant_fields: + variant_fields.append(prop_name) + seen = alternatives.setdefault(prop_name, []) + if prop_schema not in seen: + seen.append(prop_schema) + + values.append(repr(value)) + if variant_fields: + variants.append(f"{value!r} uses {', '.join(variant_fields)}") + + # Every resolved variant is a legal tag even when it adds no fields of its + # own, so the accepted values are worth advertising on their own. + if not values: + return None + + subtype_props = { + prop_name: schemas[0] if len(schemas) == 1 else {"anyOf": schemas} + for prop_name, schemas in alternatives.items() + } + properties = {**own_props, **subtype_props} + + note = f"Selects the variant. Accepted values: {', '.join(values)}." + if variants: + note += ( + f" {'; '.join(variants)}." + " Send only the fields belonging to the selected variant." + ) + + # A discriminator names a property of the payload, so give it a schema even + # when the parent left it undeclared — it is otherwise required and unusable. + tag_schema = properties.get(property_name) + if not isinstance(tag_schema, dict): + tag_schema = {"type": "string"} + existing = tag_schema.get("description") + properties[property_name] = { + **tag_schema, + "description": f"{existing} {note}" if existing else note, + } + + return properties + + def _combine_schemas_and_map_params( route: HTTPRoute, convert_refs: bool = True, @@ -329,6 +441,17 @@ def _combine_schemas_and_map_params( # Remove the allOf since we've merged it body_schema.pop("allOf", None) + # Merge discriminated subtype fields in as optional. The discriminator + # itself is dropped: its mapping points at definitions that are pruned + # from $defs once nothing references them, which would leave the + # emitted schema with dangling refs. + flattened_props = _flatten_discriminator_subtypes( + body_schema, route.request_schemas + ) + if flattened_props is not None: + body_schema["properties"] = flattened_props + body_schema.pop("discriminator", None) + body_props = body_schema.get("properties", {}) # Detect collisions: parameters that exist in multiple non-body locations diff --git a/tests/server/providers/openapi/test_openapi_discriminator.py b/tests/server/providers/openapi/test_openapi_discriminator.py new file mode 100644 index 000000000..9eca4ece0 --- /dev/null +++ b/tests/server/providers/openapi/test_openapi_discriminator.py @@ -0,0 +1,286 @@ +"""Tests for OpenAPI discriminator handling in OpenAPIProvider.""" + +import json +from typing import Any + +import httpx2 +import pytest + +from fastmcp import FastMCP +from fastmcp.client import Client +from fastmcp.server.providers.openapi import OpenAPIProvider + + +def create_openapi_server(openapi_spec: dict, client) -> FastMCP: + """Helper to create a FastMCP server with OpenAPIProvider.""" + mcp = FastMCP("OpenAPI Server") + mcp.add_provider(OpenAPIProvider(openapi_spec=openapi_spec, client=client)) + return mcp + + +def discriminator_spec( + mapping: dict[str, str] | None = None, + body_ref: str = "Pet", +) -> dict[str, Any]: + """A parent schema with a discriminator mapping onto two allOf subtypes.""" + if mapping is None: + mapping = { + "cat": "#/components/schemas/Cat", + "dog": "#/components/schemas/Dog", + } + return { + "openapi": "3.1.0", + "info": {"title": "Pet API", "version": "1.0.0"}, + "servers": [{"url": "https://api.example.com"}], + "paths": { + "/pets": { + "post": { + "operationId": "create_pet", + "requestBody": { + "required": True, + "content": { + "application/json": { + "schema": {"$ref": f"#/components/schemas/{body_ref}"} + } + }, + }, + "responses": {"200": {"description": "Created"}}, + } + } + }, + "components": { + "schemas": { + "Pet": { + "type": "object", + "properties": {"petType": {"type": "string"}}, + "required": ["petType"], + "discriminator": { + "propertyName": "petType", + "mapping": mapping, + }, + }, + "Cat": { + "allOf": [ + {"$ref": "#/components/schemas/Pet"}, + { + "type": "object", + "properties": {"meowVolume": {"type": "integer"}}, + "required": ["meowVolume"], + }, + ] + }, + "Dog": { + "allOf": [ + {"$ref": "#/components/schemas/Pet"}, + { + "type": "object", + "properties": {"packSize": {"type": "integer"}}, + "required": ["packSize"], + }, + ] + }, + } + }, + } + + +def colliding_variant_spec() -> dict[str, Any]: + """Subtypes that disagree about the shape of the discriminator property. + + The parent marks ``kind`` required without declaring it, so each subtype's + own ``const`` is the only schema available for that field. + """ + return { + "openapi": "3.1.0", + "info": {"title": "Pet API", "version": "1.0.0"}, + "servers": [{"url": "https://api.example.com"}], + "paths": { + "/pets": { + "post": { + "operationId": "create_pet", + "requestBody": { + "required": True, + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Pet"} + } + }, + }, + "responses": {"200": {"description": "Created"}}, + } + } + }, + "components": { + "schemas": { + "Pet": { + "type": "object", + "required": ["kind"], + "discriminator": { + "propertyName": "kind", + "mapping": {"cat": "Cat", "dog": "Dog"}, + }, + }, + "Cat": { + "allOf": [ + {"$ref": "#/components/schemas/Pet"}, + { + "type": "object", + "properties": { + "kind": {"const": "cat"}, + "meowVolume": {"type": "integer"}, + }, + }, + ] + }, + "Dog": { + "allOf": [ + {"$ref": "#/components/schemas/Pet"}, + { + "type": "object", + "properties": { + "kind": {"const": "dog"}, + "packSize": {"type": "integer"}, + }, + }, + ] + }, + } + }, + } + + +def propertyless_variant_spec() -> dict[str, Any]: + """Subtypes that add nothing beyond the parent they compose.""" + spec = discriminator_spec() + for name in ("Cat", "Dog"): + spec["components"]["schemas"][name] = { + "allOf": [{"$ref": "#/components/schemas/Pet"}] + } + return spec + + +async def tool_schema(spec: dict[str, Any]) -> dict[str, Any]: + """Build the server and return the generated input schema for create_pet.""" + async with httpx2.AsyncClient( + transport=httpx2.MockTransport( + lambda request: httpx2.Response(200, json={"ok": True}) + ), + base_url="https://api.example.com", + ) as client: + server = create_openapi_server(spec, client) + async with Client(server) as mcp_client: + tools = await mcp_client.list_tools() + return next(t for t in tools if t.name == "create_pet").input_schema + + +class TestDiscriminatorRequestBodies: + """Subtypes named by a discriminator mapping are flattened in as optional.""" + + async def test_subtype_fields_are_advertised(self): + """Fields reachable only through discriminator.mapping reach the schema.""" + schema = await tool_schema(discriminator_spec()) + + assert schema["properties"].keys() >= {"petType", "meowVolume", "packSize"} + + async def test_subtype_fields_are_optional(self): + """Only the discriminator is required; variant fields never are.""" + schema = await tool_schema(discriminator_spec()) + + assert schema["required"] == ["petType"] + + async def test_discriminator_property_describes_the_variants(self): + """The discriminator names which fields belong to which variant.""" + schema = await tool_schema(discriminator_spec()) + + description = schema["properties"]["petType"]["description"] + assert "meowVolume" in description + assert "packSize" in description + + async def test_discriminator_keyword_is_dropped(self): + """The mapping points at $defs that get pruned, so it cannot survive.""" + schema = await tool_schema(discriminator_spec()) + + assert "discriminator" not in schema + assert "discriminator" not in schema["properties"]["petType"] + + @pytest.mark.parametrize( + "mapping", + [ + pytest.param({"cat": "#/components/schemas/Missing"}, id="missing_ref"), + pytest.param({"cat": "Missing"}, id="missing_name"), + pytest.param({"cat": "https://example.com/Cat"}, id="remote_target"), + pytest.param({"cat": "#/definitions/Cat"}, id="unsupported_pointer"), + ], + ) + async def test_unresolvable_mapping_is_ignored(self, mapping: dict[str, str]): + """An unusable mapping leaves the parent schema as it was.""" + schema = await tool_schema(discriminator_spec(mapping=mapping)) + + assert set(schema["properties"]) == {"petType"} + + async def test_bare_schema_name_mapping_resolves(self): + """Mapping values may be schema names, not just references.""" + schema = await tool_schema( + discriminator_spec(mapping={"cat": "Cat", "dog": "Dog"}) + ) + + assert schema["properties"].keys() >= {"petType", "meowVolume", "packSize"} + + async def test_selected_variant_field_reaches_the_request_body(self): + """The reported failure: meowVolume must reach the upstream API.""" + received: dict[str, object] = {} + + def handler(request): + received["body"] = json.loads(request.content) + return httpx2.Response(200, json={"ok": True}) + + async with httpx2.AsyncClient( + transport=httpx2.MockTransport(handler), + base_url="https://api.example.com", + ) as client: + server = create_openapi_server(discriminator_spec(), client) + async with Client(server) as mcp_client: + result = await mcp_client.call_tool( + "create_pet", {"petType": "cat", "meowVolume": 11} + ) + + assert result.structured_content == {"ok": True} + assert received["body"] == {"petType": "cat", "meowVolume": 11} + + async def test_accepted_values_are_advertised(self): + """The legal tags are named even when no variant adds a field.""" + schema = await tool_schema(propertyless_variant_spec()) + + description = schema["properties"]["petType"]["description"] + assert "'cat'" in description + assert "'dog'" in description + + async def test_propertyless_variant_is_still_named(self): + """A variant adding no fields remains a legal discriminator value.""" + spec = discriminator_spec() + spec["components"]["schemas"]["Dog"] = { + "allOf": [{"$ref": "#/components/schemas/Pet"}] + } + + schema = await tool_schema(spec) + + description = schema["properties"]["petType"]["description"] + assert "'dog'" in description + assert "meowVolume" in description + + async def test_conflicting_variant_schemas_are_unioned(self): + """No variant's constraint may be advertised as if it applied to all.""" + schema = await tool_schema(colliding_variant_spec()) + + kind = schema["properties"]["kind"] + assert [alternative.get("const") for alternative in kind["anyOf"]] == [ + "cat", + "dog", + ] + + async def test_subtype_body_is_unaffected(self): + """A body referencing the child still resolves through allOf only.""" + schema = await tool_schema(discriminator_spec(body_ref="Cat")) + + assert set(schema["properties"]) == {"petType", "meowVolume"} + assert sorted(schema["required"]) == ["meowVolume", "petType"] From d382943012651391c13c81b16eb4474a19059a9e Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Mon, 27 Jul 2026 16:07:39 -0400 Subject: [PATCH 06/53] Note that review comment threads should get an acknowledgement (#4678) --- CLAUDE.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CLAUDE.md b/CLAUDE.md index 1aff501e2..2b29308ba 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -68,6 +68,7 @@ When modifying MCP functionality, changes typically need to be applied across al - **NEVER** merge a PR marked as do-not-merge or draft. Check title, body, AND labels for `[DNM]`, `DNM`, `DO NOT MERGE`, `DON'T MERGE`, `DONT MERGE`, `do-not-merge`, `dont-merge`, `[DRAFT]`, or `DRAFT` (case-insensitive, any variation — some authors use `[DRAFT]` in the title even when `isDraft` is false). Authors use these as hard stops — respect them even if CI is green and review looks clean. When triaging a batch of PRs, filter these out up front AND re-check each one's labels immediately before merging, since labels can change mid-session. - **ALWAYS** read review-bot comments before approving a PR. CodeRabbit and chatgpt-codex-connector (Codex) leave substantive review comments on most PRs in this repo — these bots have read the diff and often flag real issues that aren't in the PR description. Use `gh pr view --comments` and read the bot feedback as part of review. Unlike proposed solutions from issue reporters, review-bot feedback should be evaluated on its merits, not discounted. - **Be constructively skeptical of bot review comments on your own PRs.** CodeRabbit, Codex, and claude[bot] run a fresh review pass on every push, which means a PR with active churn can accumulate bot comments in a stream that never really ends — each fix surfaces a new edge case the next pass can flag. Most of the early feedback is real and worth acting on; diminishing returns set in fast. Evaluate each comment on its merits, the same way you would a human reviewer: is this a real bug users will hit, or a hypothetical that requires an adversarial setup? Does the fix introduce more complexity than the problem? Has the bot missed context that's obvious to a human reader (a `*,` keyword-only marker, a design decision documented elsewhere, something already resolved on a later commit)? When a comment is pedantic, a false positive, or flagging something already fixed, reply on the thread explaining the reasoning and move on — don't keep iterating just because more comments arrive. If you find yourself three rounds deep and the feedback is shifting toward "what if someone does X" hypotheticals, you're past the point where each fix is improving the PR. Stop, document the contract as-is, and ship. +- **Post a short acknowledgement on every review comment thread.** A few words are enough — that you agree and are taking it, or the reasoning when you're declining. Acknowledge as you read; don't hold the reply until a fix is committed. The bots don't meaningfully read replies, so the audience is the next person to open the PR: without a reply they can't tell whether a comment was weighed and rejected or simply missed. ### Outbound Comments and Shell Interpolation From f4ae8bb0af04cb315eef262d38433af4b71d9c38 Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Mon, 27 Jul 2026 16:07:54 -0400 Subject: [PATCH 07/53] Let maintenance releases publish without fastmcp-tasks (#4676) --- .github/workflows/publish-fastmcp-tasks.yml | 17 +++++++++++++++++ .github/workflows/publish-fastmcp.yml | 11 +++++++++-- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/.github/workflows/publish-fastmcp-tasks.yml b/.github/workflows/publish-fastmcp-tasks.yml index 9aa5326a7..29fc38554 100644 --- a/.github/workflows/publish-fastmcp-tasks.yml +++ b/.github/workflows/publish-fastmcp-tasks.yml @@ -23,13 +23,29 @@ jobs: fetch-depth: 0 ref: ${{ github.event.workflow_run.head_sha || github.sha }} + # Maintenance branches predate the standalone fastmcp-tasks package and + # resolve the `tasks` extra through fastmcp-slim instead. This workflow + # runs from the default branch for every fastmcp-slim release, including + # those tags, so detect the package rather than assume it is there. + - name: Check whether this ref builds fastmcp-tasks + id: package_present + run: | + if [ -d fastmcp_tasks ]; then + echo "present=true" >> "$GITHUB_OUTPUT" + else + echo "present=false" >> "$GITHUB_OUTPUT" + echo "This ref has no fastmcp_tasks package; nothing to publish." + fi + - name: Install uv uses: astral-sh/setup-uv@v7 - name: Build fastmcp-tasks + if: steps.package_present.outputs.present == 'true' run: uv build --package fastmcp-tasks - name: Verify matching fastmcp-slim is published + if: steps.package_present.outputs.present == 'true' run: | SLIM_VERSION=$(python - <<'PY' import email.parser @@ -84,4 +100,5 @@ jobs: exit 1 - name: Publish fastmcp-tasks to PyPI + if: steps.package_present.outputs.present == 'true' run: uv publish -v dist/fastmcp_tasks-*.tar.gz dist/fastmcp_tasks-*.whl diff --git a/.github/workflows/publish-fastmcp.yml b/.github/workflows/publish-fastmcp.yml index 52b319d5c..dc32179cb 100644 --- a/.github/workflows/publish-fastmcp.yml +++ b/.github/workflows/publish-fastmcp.yml @@ -134,17 +134,24 @@ jobs: # fastmcp-tasks is pinned via the optional `tasks` extra, so its # Requires-Dist entry carries an `extra == "tasks"` marker — unlike the # base slim dependency, do not skip marked entries here. + # + # Print nothing when there is no such pin. Release lines that resolve + # the `tasks` extra through fastmcp-slim instead of a standalone + # fastmcp-tasks package have nothing here to verify. for value in metadata.get_all("Requires-Dist", []): requirement, _, _marker = value.partition(";") match = re.fullmatch(r"fastmcp-tasks==([^;\s]+)", requirement.strip()) if match: print(match.group(1)) break - else: - raise RuntimeError("Could not find the fastmcp-tasks extra dependency") PY ) + if [ -z "$TASKS_VERSION" ]; then + echo "This build does not pin fastmcp-tasks; the [tasks] extra cannot be uninstallable, so there is nothing to verify." + exit 0 + fi + for attempt in {1..12}; do if python - "$TASKS_VERSION" <<'PY' import json From 90ea26f3372b320f367f0f7f7ad2def2051a8a0e Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Mon, 27 Jul 2026 21:06:12 -0400 Subject: [PATCH 08/53] Soften the review-comment reply guidance (#4683) --- CLAUDE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CLAUDE.md b/CLAUDE.md index 2b29308ba..45957dce3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -68,7 +68,7 @@ When modifying MCP functionality, changes typically need to be applied across al - **NEVER** merge a PR marked as do-not-merge or draft. Check title, body, AND labels for `[DNM]`, `DNM`, `DO NOT MERGE`, `DON'T MERGE`, `DONT MERGE`, `do-not-merge`, `dont-merge`, `[DRAFT]`, or `DRAFT` (case-insensitive, any variation — some authors use `[DRAFT]` in the title even when `isDraft` is false). Authors use these as hard stops — respect them even if CI is green and review looks clean. When triaging a batch of PRs, filter these out up front AND re-check each one's labels immediately before merging, since labels can change mid-session. - **ALWAYS** read review-bot comments before approving a PR. CodeRabbit and chatgpt-codex-connector (Codex) leave substantive review comments on most PRs in this repo — these bots have read the diff and often flag real issues that aren't in the PR description. Use `gh pr view --comments` and read the bot feedback as part of review. Unlike proposed solutions from issue reporters, review-bot feedback should be evaluated on its merits, not discounted. - **Be constructively skeptical of bot review comments on your own PRs.** CodeRabbit, Codex, and claude[bot] run a fresh review pass on every push, which means a PR with active churn can accumulate bot comments in a stream that never really ends — each fix surfaces a new edge case the next pass can flag. Most of the early feedback is real and worth acting on; diminishing returns set in fast. Evaluate each comment on its merits, the same way you would a human reviewer: is this a real bug users will hit, or a hypothetical that requires an adversarial setup? Does the fix introduce more complexity than the problem? Has the bot missed context that's obvious to a human reader (a `*,` keyword-only marker, a design decision documented elsewhere, something already resolved on a later commit)? When a comment is pedantic, a false positive, or flagging something already fixed, reply on the thread explaining the reasoning and move on — don't keep iterating just because more comments arrive. If you find yourself three rounds deep and the feedback is shifting toward "what if someone does X" hypotheticals, you're past the point where each fix is improving the PR. Stop, document the contract as-is, and ship. -- **Post a short acknowledgement on every review comment thread.** A few words are enough — that you agree and are taking it, or the reasoning when you're declining. Acknowledge as you read; don't hold the reply until a fix is committed. The bots don't meaningfully read replies, so the audience is the next person to open the PR: without a reply they can't tell whether a comment was weighed and rejected or simply missed. +- **It's polite to reply briefly to a review comment once you've addressed it or decided not to.** A few words — "fixed in abc1234", or the reason if you're leaving it. Nothing depends on it; the timeline already shows whether commits followed the comment. ### Outbound Comments and Shell Interpolation From 78c61415b51cb546f3570991b3fc8680a4b13236 Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Mon, 27 Jul 2026 21:41:02 -0400 Subject: [PATCH 09/53] Resolve review threads on fix, reply on decline (#4685) * Resolve review threads on fix, reply on decline * Use a placeholder PR number in the resolve example --- CLAUDE.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/CLAUDE.md b/CLAUDE.md index 45957dce3..e6357e06b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -68,7 +68,12 @@ When modifying MCP functionality, changes typically need to be applied across al - **NEVER** merge a PR marked as do-not-merge or draft. Check title, body, AND labels for `[DNM]`, `DNM`, `DO NOT MERGE`, `DON'T MERGE`, `DONT MERGE`, `do-not-merge`, `dont-merge`, `[DRAFT]`, or `DRAFT` (case-insensitive, any variation — some authors use `[DRAFT]` in the title even when `isDraft` is false). Authors use these as hard stops — respect them even if CI is green and review looks clean. When triaging a batch of PRs, filter these out up front AND re-check each one's labels immediately before merging, since labels can change mid-session. - **ALWAYS** read review-bot comments before approving a PR. CodeRabbit and chatgpt-codex-connector (Codex) leave substantive review comments on most PRs in this repo — these bots have read the diff and often flag real issues that aren't in the PR description. Use `gh pr view --comments` and read the bot feedback as part of review. Unlike proposed solutions from issue reporters, review-bot feedback should be evaluated on its merits, not discounted. - **Be constructively skeptical of bot review comments on your own PRs.** CodeRabbit, Codex, and claude[bot] run a fresh review pass on every push, which means a PR with active churn can accumulate bot comments in a stream that never really ends — each fix surfaces a new edge case the next pass can flag. Most of the early feedback is real and worth acting on; diminishing returns set in fast. Evaluate each comment on its merits, the same way you would a human reviewer: is this a real bug users will hit, or a hypothetical that requires an adversarial setup? Does the fix introduce more complexity than the problem? Has the bot missed context that's obvious to a human reader (a `*,` keyword-only marker, a design decision documented elsewhere, something already resolved on a later commit)? When a comment is pedantic, a false positive, or flagging something already fixed, reply on the thread explaining the reasoning and move on — don't keep iterating just because more comments arrive. If you find yourself three rounds deep and the feedback is shifting toward "what if someone does X" hypotheticals, you're past the point where each fix is improving the PR. Stop, document the contract as-is, and ship. -- **It's polite to reply briefly to a review comment once you've addressed it or decided not to.** A few words — "fixed in abc1234", or the reason if you're leaving it. Nothing depends on it; the timeline already shows whether commits followed the comment. +- **Resolve a review thread when you fix it; reply when you're declining it.** A fix explains itself through the commit, so resolving is enough — and it leaves unresolved threads meaning unfinished business, which is the signal worth having. A decline needs a one-line reason in a reply, because resolving collapses the thread and a hidden objection is worse than a visible one. Doing both is noise. Get thread ids from the GraphQL `reviewThreads` field, then resolve: + + ```bash + gh api graphql -f query='query($n:Int!){repository(owner:"PrefectHQ",name:"fastmcp"){pullRequest(number:$n){reviewThreads(first:50){nodes{id isResolved path}}}}}' -F n= + gh api graphql -f query='mutation($id:ID!){resolveReviewThread(input:{threadId:$id}){thread{isResolved}}}' -F id=PRRT_... + ``` ### Outbound Comments and Shell Interpolation From d6b9daecb191a971eac236be9ea6b784a8c6cc97 Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Tue, 28 Jul 2026 08:12:17 -0400 Subject: [PATCH 10/53] Read CLI-scanned MCP config files as UTF-8 explicitly (#4690) --- fastmcp_slim/fastmcp/cli/discovery.py | 6 ++--- tests/cli/test_discovery.py | 34 +++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/fastmcp_slim/fastmcp/cli/discovery.py b/fastmcp_slim/fastmcp/cli/discovery.py index 5acd42d61..ff3c616ff 100644 --- a/fastmcp_slim/fastmcp/cli/discovery.py +++ b/fastmcp_slim/fastmcp/cli/discovery.py @@ -120,7 +120,7 @@ def _parse_mcp_servers( def _parse_mcp_config(path: Path, source: str) -> list[DiscoveredServer]: """Parse an mcpServers-style JSON file into discovered servers.""" try: - text = path.read_text() + text = path.read_text(encoding="utf-8") except OSError as exc: logger.debug("Could not read %s: %s", path, exc) return [] @@ -158,7 +158,7 @@ def _scan_claude_code(start_dir: Path) -> list[DiscoveredServer]: """Scan ``~/.claude.json`` for global and project-scoped MCP servers.""" path = Path.home() / ".claude.json" try: - text = path.read_text() + text = path.read_text(encoding="utf-8") except OSError: return [] @@ -269,7 +269,7 @@ def _scan_goose() -> list[DiscoveredServer]: path = config_dir / "config.yaml" try: - text = path.read_text() + text = path.read_text(encoding="utf-8") except OSError: return [] diff --git a/tests/cli/test_discovery.py b/tests/cli/test_discovery.py index 716694353..102bbf440 100644 --- a/tests/cli/test_discovery.py +++ b/tests/cli/test_discovery.py @@ -148,6 +148,40 @@ class TestParseMcpConfig: assert isinstance(servers[0].config, RemoteMCPServer) assert servers[0].config.url == "http://localhost:8000/mcp" + def test_reads_as_utf8_explicitly( + self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch + ): + """Regression test for GH-4689: config files must be read with an + explicit UTF-8 encoding, not the platform's preferred encoding + (e.g. cp949 on Windows with a non-UTF-8 locale), since that's what + every tool that writes these files emits.""" + original_read_text = Path.read_text + + def _tracking_read_text(self: Path, *args: Any, **kwargs: Any) -> str: + assert kwargs.get("encoding") == "utf-8", ( + "path.read_text() must pass encoding='utf-8' explicitly" + ) + return original_read_text(self, *args, **kwargs) + + monkeypatch.setattr(Path, "read_text", _tracking_read_text) + + path = tmp_path / "config.json" + path.write_bytes( + json.dumps( + { + "mcpServers": { + "demo": { + "command": "echo", + "args": ["hello — world"], + } + } + } + ).encode("utf-8") + ) + servers = _parse_mcp_config(path, "test") + assert len(servers) == 1 + assert servers[0].name == "demo" + # --------------------------------------------------------------------------- # Scanner: Claude Desktop From 0175bc9235cf831149ef8712d45c51b51973e8e0 Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Tue, 28 Jul 2026 10:47:39 -0400 Subject: [PATCH 11/53] Split the SDK upgrade guides by SDK version (#4684) --- README.md | 7 +- docs/docs.json | 20 +- docs/getting-started/installation.mdx | 12 +- .../upgrading/from-fastmcp-2.mdx | 58 +- .../upgrading/from-fastmcp-3.mdx | 235 +++++-- ...evel-sdk.mdx => from-low-level-sdk-v1.mdx} | 166 +++-- .../upgrading/from-low-level-sdk-v2.mdx | 622 ++++++++++++++++++ .../upgrading/from-mcp-sdk-v1.mdx | 264 ++++++++ .../upgrading/from-mcp-sdk-v2.mdx | 325 +++++++++ .../upgrading/from-mcp-sdk.mdx | 166 ----- docs/servers/elicitation.mdx | 4 +- fastmcp_slim/README.md | 7 +- tests/docs/test_upgrade_guide_api_claims.py | 250 +++++++ tests/docs/test_upgrade_guide_equivalence.py | 239 +++++++ tests/docs/test_upgrade_guide_examples.py | 101 +++ 15 files changed, 2162 insertions(+), 314 deletions(-) rename docs/getting-started/upgrading/{from-low-level-sdk.mdx => from-low-level-sdk-v1.mdx} (61%) create mode 100644 docs/getting-started/upgrading/from-low-level-sdk-v2.mdx create mode 100644 docs/getting-started/upgrading/from-mcp-sdk-v1.mdx create mode 100644 docs/getting-started/upgrading/from-mcp-sdk-v2.mdx delete mode 100644 docs/getting-started/upgrading/from-mcp-sdk.mdx create mode 100644 tests/docs/test_upgrade_guide_api_claims.py create mode 100644 tests/docs/test_upgrade_guide_equivalence.py create mode 100644 tests/docs/test_upgrade_guide_examples.py diff --git a/README.md b/README.md index b2b8c1886..e29ce7a9b 100644 --- a/README.md +++ b/README.md @@ -100,9 +100,10 @@ uv pip install fastmcp For full installation instructions, including verification and upgrading, see the [**Installation Guide**](https://gofastmcp.com/getting-started/installation). **Upgrading?** We have guides for: -- [Upgrading from FastMCP v2](https://gofastmcp.com/getting-started/upgrading/from-fastmcp-2) -- [Upgrading from the MCP Python SDK](https://gofastmcp.com/getting-started/upgrading/from-mcp-sdk) -- [Upgrading from the low-level SDK](https://gofastmcp.com/getting-started/upgrading/from-low-level-sdk) +- [Upgrading from FastMCP 3](https://gofastmcp.com/getting-started/upgrading/from-fastmcp-3) +- [Upgrading from FastMCP 2](https://gofastmcp.com/getting-started/upgrading/from-fastmcp-2) +- [Upgrading from MCP SDK v1](https://gofastmcp.com/getting-started/upgrading/from-mcp-sdk-v1) or [v2](https://gofastmcp.com/getting-started/upgrading/from-mcp-sdk-v2) +- [Upgrading from the low-level SDK v1](https://gofastmcp.com/getting-started/upgrading/from-low-level-sdk-v1) or [v2](https://gofastmcp.com/getting-started/upgrading/from-low-level-sdk-v2) > [!NOTE] > If `import fastmcp` fails right after a `pip` upgrade from FastMCP 3.2 or earlier, run `pip install --force-reinstall fastmcp`. See [Troubleshooting](https://gofastmcp.com/getting-started/installation#troubleshooting) for why this happens (`uv` is unaffected). diff --git a/docs/docs.json b/docs/docs.json index 429e97fd3..7b560be71 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -358,10 +358,12 @@ "group": "Upgrading", "icon": "up", "pages": [ - "getting-started/upgrading/from-fastmcp-2", "getting-started/upgrading/from-fastmcp-3", - "getting-started/upgrading/from-mcp-sdk", - "getting-started/upgrading/from-low-level-sdk" + "getting-started/upgrading/from-fastmcp-2", + "getting-started/upgrading/from-mcp-sdk-v1", + "getting-started/upgrading/from-mcp-sdk-v2", + "getting-started/upgrading/from-low-level-sdk-v1", + "getting-started/upgrading/from-low-level-sdk-v2" ] }, { @@ -509,13 +511,21 @@ "source": "/development/upgrade-guide" }, { - "destination": "/getting-started/upgrading/from-mcp-sdk", + "destination": "/getting-started/upgrading/from-mcp-sdk-v1", "source": "/getting-started/upgrading-from-sdk" }, { - "destination": "/getting-started/upgrading/from-low-level-sdk", + "destination": "/getting-started/upgrading/from-mcp-sdk-v1", + "source": "/getting-started/upgrading/from-mcp-sdk" + }, + { + "destination": "/getting-started/upgrading/from-low-level-sdk-v1", "source": "/getting-started/low-level-sdk" }, + { + "destination": "/getting-started/upgrading/from-low-level-sdk-v1", + "source": "/getting-started/upgrading/from-low-level-sdk" + }, { "destination": "/getting-started/upgrading/from-fastmcp-3", "source": "/getting-started/upgrading/to-mcp-sdk-v2" diff --git a/docs/getting-started/installation.mdx b/docs/getting-started/installation.mdx index 4dae8e9b7..9bfbdbcb1 100644 --- a/docs/getting-started/installation.mdx +++ b/docs/getting-started/installation.mdx @@ -68,13 +68,17 @@ See the [Upgrade Guide](/getting-started/upgrading/from-fastmcp-2) for a complet ### From the MCP SDK -#### From FastMCP 1.0 +Which guide you want depends on which `mcp` version you're on and which of its two server APIs you used. -If you're using FastMCP 1.0 via the `mcp` package (meaning you import FastMCP as `from mcp.server.fastmcp import FastMCP`), upgrading is straightforward — for most servers, it's a single import change. See the [full upgrade guide](/getting-started/upgrading/from-mcp-sdk) for details. +#### From the high-level server -#### From the Low-Level Server API +If you're using FastMCP 1.0 via SDK v1 (meaning you import FastMCP as `from mcp.server.fastmcp import FastMCP`), upgrading is straightforward — for most servers it's a single import change. See [Upgrading from MCP SDK v1](/getting-started/upgrading/from-mcp-sdk-v1), which also explains why that route is usually easier than moving to MCP SDK v2. -If you built your server directly on the `mcp` package's `Server` class — with `list_tools()`/`call_tool()` handlers and hand-written JSON Schema — see the [migration guide](/getting-started/upgrading/from-low-level-sdk) for a full walkthrough. +If you already moved to SDK v2 and write against `MCPServer`, see [Upgrading from MCP SDK v2](/getting-started/upgrading/from-mcp-sdk-v2) — that migration is mostly renaming. + +#### From the low-level server + +If you built your server directly on the `mcp` package's `Server` class, the guide you want depends on how its handlers are registered. Decorators like `@server.list_tools()` mean SDK v1 — see [Upgrading from the Low-Level SDK v1](/getting-started/upgrading/from-low-level-sdk-v1). Handlers passed to the constructor as `on_list_tools=` mean SDK v2 — see [Upgrading from the Low-Level SDK v2](/getting-started/upgrading/from-low-level-sdk-v2). ## Troubleshooting diff --git a/docs/getting-started/upgrading/from-fastmcp-2.mdx b/docs/getting-started/upgrading/from-fastmcp-2.mdx index 8550413bc..98f0b4883 100644 --- a/docs/getting-started/upgrading/from-fastmcp-2.mdx +++ b/docs/getting-started/upgrading/from-fastmcp-2.mdx @@ -1,11 +1,15 @@ --- title: Upgrading from FastMCP 2 sidebarTitle: "From FastMCP 2" -description: Migration instructions for upgrading between FastMCP versions +description: What changed in FastMCP 3 for servers written against FastMCP 2 icon: up --- -This guide covers breaking changes and migration steps when upgrading FastMCP. +This guide covers the breaking changes a FastMCP 2 server meets on its way to FastMCP 3, newest release first. + + +**Going all the way to FastMCP 4?** You need this page and [Upgrading from FastMCP 3](/getting-started/upgrading/from-fastmcp-3), in that order. The two describe different transitions: this one covers the v3 API changes, while the FastMCP 3 guide covers the MCP Python SDK v2 rebuild underneath v4. Where a v3 deprecation was later removed outright, this page marks it **Removed in v4**. + ## v3.0.0 @@ -101,7 +105,7 @@ For each issue found, show the original line, explain why it breaks, and provide In v2, you could configure transport settings directly in the `FastMCP()` constructor. In v3, `FastMCP()` is purely about your server's identity and behavior — transport configuration happens when you actually start serving. Passing any of the old kwargs now raises `TypeError` with a migration hint. -```python +```python test="skip" # Before mcp = FastMCP("server", host="0.0.0.0", port=8080) mcp.run() @@ -140,7 +144,7 @@ Keeping `DiskStore` requires `pip install 'py-key-value-aio[disk]'`, which re-in In v2, you could enable or disable individual components by calling methods on the component object itself. In v3, visibility is controlled through the server (or provider), which lets you target components by name, tag, or type without needing a reference to the object: -```python +```python test="skip" # Before tool = await server.get_tool("my_tool") tool.disable() @@ -155,7 +159,7 @@ Calling `.enable()` or `.disable()` on a component object now raises `NotImpleme The `get_tools()`, `get_resources()`, `get_prompts()`, and `get_resource_templates()` methods have been renamed to `list_tools()`, `list_resources()`, `list_prompts()`, and `list_resource_templates()`. More importantly, they now return lists instead of dicts — so code that indexes by name needs to change: -```python +```python test="skip" # Before tools = await server.get_tools() tool = tools["my_tool"] @@ -169,7 +173,7 @@ tool = next((t for t in tools if t.name == "my_tool"), None) Prompt functions now use FastMCP's `Message` class instead of `mcp.types.PromptMessage`. The new class is simpler — it accepts a plain string and defaults to `role="user"`, so most prompts become one-liners: -```python +```python test="skip" # Before from mcp.types import PromptMessage, TextContent @@ -187,7 +191,7 @@ def my_prompt() -> Message: If your prompt functions return raw dicts with `role` and `content` keys, those also need to change. v2 silently coerced dicts into prompt messages, but v3 requires typed `Message` objects (or plain strings for single user messages): -```python +```python test="skip" # Before (v2 accepted this) @mcp.prompt def my_prompt(): @@ -211,7 +215,7 @@ def my_prompt() -> list[Message]: `ctx.set_state()` and `ctx.get_state()` are now async because state in v3 is session-scoped and backed by a pluggable storage backend (rather than a simple dict). This means state persists across multiple tool calls within the same session: -```python +```python test="skip" # Before ctx.set_state("key", "value") value = ctx.get_state("key") @@ -223,7 +227,7 @@ value = await ctx.get_state("key") State values must also be JSON-serializable by default (dicts, lists, strings, numbers, etc.). If you need to store non-serializable values like an HTTP client, pass `serializable=False` — these values are request-scoped and only available during the current tool call: -```python +```python test="skip" await ctx.set_state("client", my_http_client, serializable=False) ``` @@ -245,7 +249,7 @@ parent.mount(child, namespace="child") In v2, auth providers like `GitHubProvider` could auto-load configuration from environment variables with a `FASTMCP_SERVER_AUTH_*` prefix. This magic has been removed — pass values explicitly: -```python +```python test="skip" # Before (v2) — client_id and client_secret loaded automatically # from FASTMCP_SERVER_AUTH_GITHUB_CLIENT_ID, etc. auth = GitHubProvider() @@ -278,7 +282,7 @@ transport = StreamableHttpTransport("http://localhost:8000/mcp") `OpenAPIProvider` no longer accepts a `timeout` parameter. Configure timeout on the httpx2 client directly. The `client` parameter is also now optional — when omitted, a default client is created from the spec's `servers` URL with a 30-second timeout: -```python +```python test="skip" # Before provider = OpenAPIProvider(spec, client, timeout=60) @@ -291,7 +295,7 @@ provider = OpenAPIProvider(spec, client) The FastMCP metadata key in component `meta` dicts changed from `_fastmcp` to `fastmcp`. If you read metadata from tool or resource objects, update the key: -```python +```python test="skip" # Before tags = tool.meta.get("_fastmcp", {}).get("tags", []) @@ -309,7 +313,7 @@ Metadata is now always included — the `include_fastmcp_meta` parameter has bee In v2, `@mcp.tool` transformed your function into a `FunctionTool` object. In v3, decorators return your original function unchanged — which means decorated functions stay callable for testing, reuse, and composition: -```python +```python test="skip" @mcp.tool def greet(name: str) -> str: return f"Hello, {name}!" @@ -335,7 +339,7 @@ These were deprecated in v3. Items marked **Removed in v4** no longer work at al **mount() prefix → namespace** (Removed in v4) -```python +```python test="skip" # Removed in v4 main.mount(subserver, prefix="api") @@ -345,7 +349,7 @@ main.mount(subserver, namespace="api") **import_server() → mount()** (Removed in v4) -```python +```python test="skip" # Removed in v4 main.import_server(subserver) @@ -382,7 +386,7 @@ server = FastMCP("my_api", providers=[OpenAPIProvider(spec, client)]) **add_tool_transformation() → add_transform()** (Removed in v4) -```python +```python test="skip" # Removed in v4 mcp.add_tool_transformation("name", config) @@ -395,7 +399,7 @@ mcp.add_transform(ToolTransform({"name": config})) The proxy target is passed positionally in both APIs, so most calls migrate unchanged. If you passed the target by keyword, note that the parameter was renamed from `backend=` to `target=`. -```python +```python test="skip" # Removed in v4 proxy = FastMCP.as_proxy("http://example.com/mcp") proxy = FastMCP.as_proxy(backend="http://example.com/mcp") # keyword form @@ -424,12 +428,18 @@ server = FastMCP("my_api", providers=[OpenAPIProvider(spec, client)]) ### Removed Deprecated Features -- `BearerAuthProvider` → use `JWTVerifier` -- `Context.get_http_request()` → use `get_http_request()` from dependencies -- `from fastmcp import Image` → use `from fastmcp.utilities.types import Image` -- `FastMCP(dependencies=[...])` → use `fastmcp.json` configuration -- `FastMCPProxy(client=...)` → use `client_factory=lambda: ...` -- `output_schema=False` → use `output_schema=None` +A batch of long-deprecated surfaces came out in 2.14. Each fails loudly at import or call time, and each has a direct replacement: + +| Removed | Replacement | +|---|---| +| `BearerAuthProvider` | `JWTVerifier` — the same JWT validation under a name that says what it does | +| `Context.get_http_request()` | `get_http_request()` from [dependency injection](/servers/dependency-injection) | +| `from fastmcp import Image` | `from fastmcp.utilities.types import Image` | +| `FastMCP(dependencies=[...])` | a [`fastmcp.json`](/deployment/server-configuration) configuration file | +| `FastMCPProxy(client=...)` | `client_factory=lambda: ...` | +| `output_schema=False` | `output_schema=None` | + +Two of these are worth understanding rather than just swapping. `FastMCPProxy` takes a factory instead of a client because a single shared client cannot serve concurrent proxied sessions safely — the factory gives each session its own backend connection. And `output_schema=False` became `output_schema=None` because `False` read as "this tool has a schema, and it is false"; `None` says plainly that there is no schema. ## v2.13.0 @@ -437,7 +447,7 @@ server = FastMCP("my_api", providers=[OpenAPIProvider(spec, client)]) The OAuth proxy now issues its own JWT tokens. For production, provide explicit keys: -```python +```python test="skip" auth = GitHubProvider( client_id=os.environ["GITHUB_CLIENT_ID"], client_secret=os.environ["GITHUB_CLIENT_SECRET"], diff --git a/docs/getting-started/upgrading/from-fastmcp-3.mdx b/docs/getting-started/upgrading/from-fastmcp-3.mdx index 010718fc9..912f5b37b 100644 --- a/docs/getting-started/upgrading/from-fastmcp-3.mdx +++ b/docs/getting-started/upgrading/from-fastmcp-3.mdx @@ -1,15 +1,17 @@ --- title: Upgrading from FastMCP 3 -sidebarTitle: "From FastMCP 3.x" +sidebarTitle: "From FastMCP 3" description: What changes when you upgrade to FastMCP 4, which builds on the MCP Python SDK v2 icon: up --- FastMCP 4 builds on the MCP Python SDK v2, and that is the source of every change in this guide. The SDK v2 makes two sweeping changes to the protocol layer: it splits the protocol types out of `mcp.types` into a standalone `mcp_types` package, and it renames every protocol field from camelCase to snake_case (`inputSchema` → `input_schema`, `mimeType` → `mime_type`, `isError` → `is_error`, and so on). -FastMCP 4 absorbs almost all of this for you. Field access is bridged so your existing reads keep working, and the imports you were taught have a stable home in FastMCP itself. The sections below describe what FastMCP handles for you, the small number of changes you must make in your own code, and the deprecation timeline for the compatibility shims. +FastMCP 4 absorbs almost all of this for you. Field access is bridged so your existing reads keep working, and the imports you were taught have a stable home in FastMCP itself. What the SDK cannot hide is the protocol's own direction: the new sessionless era removes the server's ability to call back into a client mid-request, and background tasks moved out of the core spec into an extension. Those two shape the changes a working server is most likely to feel. -## Install the v4 prerelease +The sections below cover what FastMCP handles for you, the changes you must make in your own code, the surfaces removed outright in 4.0, the behavior shifts that compile fine but act differently, and the deprecation timeline for the compatibility shims. + +## Install the v4 Prerelease While FastMCP 4 is in prerelease, pin the beta and its prerelease protocol dependencies explicitly. For a uv project, add the following to `pyproject.toml`: @@ -27,26 +29,92 @@ constraint-dependencies = [ Then run `uv lock` or `uv sync` normally. The constraints opt only these transitive packages into their prerelease versions; you do not need `--prerelease allow`, which permits prereleases throughout the dependency graph. -## Environment requirements + +You are upgrading an MCP server or client from FastMCP 3.x to FastMCP 4, which is built on the MCP Python SDK v2. + +FIRST, fetch https://gofastmcp.com/getting-started/upgrading/from-fastmcp-3 — it explains every item below, with the replacement code. Fetch https://gofastmcp.com for anything the guide doesn't cover. Do not invent a FastMCP API you have not confirmed in the docs. + +Then search the provided code for each signal below. Most FastMCP 3 servers upgrade untouched, so report only what you actually find. + +ENVIRONMENT +- a pydantic pin below 2.12 +- a FastAPI pin below 0.133.0, the first release admitting Starlette 1.x (earlier ones cap it, e.g. 0.115.12 requires `starlette<0.47.0`), or any direct Starlette pin below 1.0.1 + +IMPORTS THAT NO LONGER RESOLVE +- `mcp.types` (anywhere, in any form) +- `fastmcp.server.proxy`, `fastmcp.server.openapi`, `FastMCPOpenAPI` +- `fastmcp.experimental.server.openapi`, `fastmcp.experimental.utilities.openapi` +- `fastmcp.experimental.sampling.handlers` +- `fastmcp.server.apps`, `fastmcp.server.app` +- `fastmcp.tools.tool`, `fastmcp.resources.resource`, `fastmcp.prompts.prompt` +- `fastmcp.server.tasks`, `fastmcp.server.sampling` +- `fastmcp.server.auth.authorization` +- `CurrentDocket` or `CurrentWorker` from `fastmcp.dependencies` +- `SkillsProvider` +- `CachableToolResult`, `CachablePromptResult`, and their siblings (the misspelling was corrected with no alias) +- `PromptToolMiddleware`, `ResourceToolMiddleware` + +REMOVED SERVER METHODS AND KEYWORDS +- `FastMCP.as_proxy(...)` +- `import_server(...)` ← flag this one loudly: `mount()` is the replacement but NOT an equivalent. `import_server` took a static snapshot and skipped the child's lifespan and middleware; `mount` is a live composition that runs both. +- `mount(prefix=...)`, `mount(as_proxy=...)` +- `add_tool_transformation(...)`, `remove_tool_transformation(...)` +- `remove_tool(...)` ← its replacement raises KeyError where this raised NotFoundError, so check surrounding except clauses +- tool `serializer=`, tool `exclude_args=` +- `StreamableHttpTransport(sse_read_timeout=...)` +- `FASTMCP_DECORATOR_MODE` / `settings.decorator_mode` +- `FastMCP(sampling_handler=...)`, `sampling_handler_behavior=` + +REMOVED CONTEXT METHODS +- `ctx.sample(...)`, `ctx.sample_step(...)`, `ctx.list_roots(...)` +- Note for the user: if borrowing the CALLER's model is the whole point of the server, the guide's recommendation is to stay on FastMCP 3.x rather than migrate. +- The client side is NOT affected — `Client(sampling_handler=...)` and `Client(roots=...)` still mean what they meant. + +RUNTIME BREAKS THAT STILL COMPILE — the ones most likely to reach production +- `ctx.elicit(...)` anywhere. It is era-gated in 4.0 and raises on modern connections, which is what `Client` now negotiates by default. This is the single most likely runtime failure. +- `ctx.elicit(...)` called without `response_type` +- `except httpx.` around any FastMCP call. FastMCP raises httpx2 exceptions now, but httpx is usually still installed transitively, so the handler imports, type-checks, and silently never matches. +- a custom `httpx.AsyncClient`, `httpx_client_factory=`, or `httpx.Auth` handed to a FastMCP transport, `OAuth`, or `from_openapi` +- `Middleware.on_initialize` hooks, and `ctx.set_state` values read back in a later call — neither survives a modern connection +- middleware assuming `on_message` only sees routable requests +- camelCase field reads (`inputSchema`, `isError`, `mimeType`, `nextCursor`, `structuredContent`, `serverInfo`, and the rest) — these still work but warn, and are scheduled for removal +- clients matching on the resource-not-found error code -32002 +- templated resources whose parameters legitimately carry `..` or absolute paths +- an OAuth server (`OAuthProxy` or anything built on it) with `issuer_url` set to something other than `base_url` — this forces a one-time re-authorization of every client + +BACKGROUND TASKS +- `@mcp.tool(task=True)` or `TaskConfig` without `mcp.add_extension(TasksExtension())` +- `task=` on a `@mcp.resource` or `@mcp.prompt` decorator (tools only now) +- `client.call_tool(..., task=True)`, `read_resource(task=True)`, `get_prompt(task=True)` + +ERRORS +- `McpError(ErrorData(...))` positional construction. Catching and `err.error.code` are unchanged; only construction moved. + +For each item found, show the original line, name what changed, and give the corrected code from the guide. Where you could not confirm a replacement in the docs, say so instead of guessing. + + +## Environment Requirements The SDK v2 raises FastMCP's dependency floors, which matters before any of your code runs. **pydantic >= 2.12 is now the floor.** If your project pins an older pydantic (for example `pydantic==2.11.*`), installing this FastMCP release fails with an unsatisfiable-resolution error from your installer — bump your pin to `>=2.12` first. If you don't pin pydantic at all, installers upgrade it silently as part of the FastMCP upgrade. -**The server extra floors Starlette >= 1.0.1.** Modern FastAPI (0.11x and later) already runs on Starlette 1.x, so mounting a FastMCP server inside a FastAPI app coexists cleanly — verified with FastAPI 0.138.2. Only very old FastAPI versions pinned below Starlette 1.0.1 conflict; upgrade FastAPI if your resolver complains about Starlette. +**The server extra floors Starlette >= 1.0.1.** This is the requirement most likely to force an unrelated upgrade, because FastAPI pinned Starlette to a sub-1.0 range for a long time — FastAPI 0.115.12, for example, requires `starlette<0.47.0`. **FastAPI 0.133.0 is the first release that admits Starlette 1.x**, so a project pinned below that gets an unsatisfiable resolution rather than a version bump. Raise your FastAPI pin to `>=0.133.0` before upgrading FastMCP. Mounting a FastMCP server inside a FastAPI app is otherwise unaffected — verified against FastAPI 0.135.2 on Starlette 1.3.1. -## What FastMCP absorbs +## What FastMCP Absorbs -### Legacy camelCase field access keeps working +### camelCase Field Access Objects that FastMCP hands back to you — the results of `client.list_tools()`, `client.call_tool_mcp()`, `client.read_resource()`, and the parameter objects passed to your sampling and elicitation handlers — are SDK v2 objects with snake_case fields. FastMCP installs a compatibility bridge at import time that routes the old camelCase names to their new snake_case fields, so code written against FastMCP 2.x still reads correctly: ```python from fastmcp import Client -async with Client("my_mcp_server.py") as client: - tools = await client.list_tools() - schema = tools[0].inputSchema # still works, warns once + +async def read_schema(): + async with Client("my_mcp_server.py") as client: + tools = await client.list_tools() + return tools[0].inputSchema # still works, warns once ``` Each bridged read emits a `FastMCPDeprecationWarning` pointing you at the snake_case name (`tools[0].input_schema` here). The bridge covers the fields users actually read: `inputSchema`/`outputSchema` on tools; `readOnlyHint`, `destructiveHint`, `idempotentHint`, and `openWorldHint` on tool annotations; `mimeType` on resources and content; `isError`/`structuredContent` on tool results; `nextCursor` on paginated results; `serverInfo`/`protocolVersion` on the initialize result; the sampling parameter fields (`systemPrompt`, `maxTokens`, `stopSequences`, `modelPreferences`, `toolChoice`); and `requestedSchema` on elicitation parameters. @@ -61,7 +129,7 @@ fastmcp.settings.mcp_camelcase_compat = False See [Settings](/more/settings) for the full reference. -### Protocol types moved to `mcp_types` +### Protocol Types The `mcp.types` module no longer exists. Every protocol type — `TextContent`, `ImageContent`, `Tool`, `ErrorData`, `Icon`, `PromptMessage`, `SamplingMessage`, `ToolAnnotations`, notification and request wrapper types like `ToolListChangedNotification`, and everything else — now lives in the standalone `mcp_types` package. Update your imports to point there: @@ -71,7 +139,7 @@ from mcp_types import TextContent, Tool, ToolAnnotations `fastmcp.types` still exists, but holds only types FastMCP defines itself (currently just `Textarea`, used to render a multiline textarea in form-based UIs) — it does not re-export protocol types. -### `McpError` has an alias +### The `McpError` Alias `fastmcp.exceptions.McpError` is an alias of the SDK's `MCPError`. Catching errors is unchanged — `except McpError` still catches SDK-raised errors, and reading `err.error.code` still works: @@ -84,7 +152,7 @@ except McpError as err: print(err.error.code) ``` -### Behavior preserved across the SDK boundary +### Preserved Behavior A few client behaviors that touch the SDK are preserved so you don't have to change anything: @@ -92,7 +160,7 @@ A few client behaviors that touch the SDK are preserved so you don't have to cha - `client.ping()` returns a `bool`. - `client.transport.get_session_id()` returns `None` on protocol eras that have no session, rather than raising. (The SDK v2 removed session-id access from its streamable HTTP transport; FastMCP reconstructs it on the transport object.) -## What you must change +## What You Must Change Everything above, FastMCP handled for you. What remains lives in your own code, where FastMCP can't reach it — your imports, how you construct errors, the custom HTTP clients you hand to a transport, and any place you reach past FastMCP's surfaces into the raw SDK objects. Each surfaces as a clear failure at import or call time, and each is a mechanical fix. @@ -112,7 +180,7 @@ TypeError: MCPError.__init__() missing 1 required positional argument: 'message' Note the message prints the class as `MCPError` (uppercase) even though your code wrote `McpError` — the old name is an alias for the SDK's renamed class. Construct the error with keyword arguments instead: -```python +```python test="skip" from fastmcp.exceptions import McpError # Before (raises TypeError under SDK v2): @@ -128,7 +196,7 @@ Catching and `err.error.code` are unchanged — only construction moved. **FastMCP now uses httpx2 exclusively.** FastMCP has replaced `httpx` with [httpx2](https://pypi.org/project/httpx2/), a next-generation httpx fork, across its entire HTTP stack — client transports and every server-side path (auth providers, the OpenAPI integration, the version check). `httpx` is no longer a FastMCP dependency. If you pass a custom client or factory into a FastMCP client transport — `StreamableHttpTransport(httpx_client_factory=...)`, `SSETransport(httpx_client_factory=...)`, `OAuth(httpx_client_factory=...)`, or a custom `httpx.Auth` as `Client(auth=...)` — those objects must now be httpx2. httpx2 is a drop-in fork with the same public API, so the change is an import swap: -```python +```python test="skip" # Before import httpx @@ -153,10 +221,12 @@ The `client` you pass to `FastMCP.from_openapi(client=...)` (and `OpenAPIProvide ```python import httpx # still installed transitively — this import works -try: - result = await client.call_tool("fetch", {"url": url}) -except httpx.ConnectError: # dead code: FastMCP now raises httpx2.ConnectError - return fallback() + +async def fetch(client, url): + try: + return await client.call_tool("fetch", {"url": url}) + except httpx.ConnectError: # dead code: FastMCP now raises httpx2.ConnectError + return fallback() ``` Grep your codebase for `except httpx.` and move those handlers to `httpx2`. The exception hierarchies match name-for-name, so the fix is an import swap — the hard part is remembering to look. One place you are covered automatically: exceptions raised *inside your tools and resources* (for example, a tool whose own old-httpx call gets a 429) are still mapped to `ToolError`/`ResourceError` by FastMCP's error boundary, which recognizes both libraries' exceptions during the transition. @@ -167,7 +237,7 @@ Two runtime behaviors shift with httpx2, and because the switch is now wholesale Deprecations that warned throughout the 3.x line are removed in 4.0. Unlike the bridged changes above, these fail immediately at the call site — a `ModuleNotFoundError`, `ImportError`, `AttributeError`, or `TypeError` — so nothing degrades silently. Every one has a direct replacement, and the fix is mechanical. -### Moved imports +### Moved Imports The proxy, OpenAPI, and app integrations moved to their permanent homes, and the internal component classes are no longer re-exported from their old aliases: @@ -188,10 +258,13 @@ The proxy, OpenAPI, and app integrations moved to their permanent homes, and the | `AuthCheck` / `AuthContext` / `require_scopes` / `require_roles` / `restrict_tag` / `run_auth_checks` from `fastmcp.server.auth.authorization` | `fastmcp.server.auth` | | `run_auth_checks_with_shortfall` / `scope_requirements` from `fastmcp.server.auth.authorization` | `fastmcp.utilities.authorization` | | `SkillsProvider` | `SkillsDirectoryProvider` from `fastmcp.server.providers.skills` | +| `TaskConfig` from `fastmcp.server.tasks` | `fastmcp.utilities.tasks` | +| `CurrentDocket` / `CurrentWorker` from `fastmcp.dependencies` | `fastmcp_tasks.dependencies` | +| `fastmcp.server.sampling` (and `SamplingTool`) | removed with [server-side sampling](#protocol-version-support) | Two renames in the same family are worth calling out because they have no compatibility alias. The response-caching wrapper models lost a spelling typo — `CachableToolResult`, `CachablePromptResult`, and their siblings became `CacheableToolResult`, `CacheablePromptResult`, etc. — so an import of the old spelling from `fastmcp.server.middleware.caching` raises `ImportError`. And `PromptToolMiddleware` / `ResourceToolMiddleware` are gone in favor of the `PromptsAsTools` / `ResourcesAsTools` transforms from `fastmcp.server.transforms` (the `ToolInjectionMiddleware` base class is retained). -### Removed server methods and `mount()` keywords +### Removed Server Methods These `FastMCP` methods and keywords have warned since 3.0 and are now removed: @@ -211,7 +284,7 @@ Two of these replacements are not exact behavioral swaps. `create_proxy` takes i `import_server` → `mount` is the one row here that is not a mechanical swap, because the two never had the same semantics. `import_server` took a **one-time static snapshot** — it copied the child's tools, resources, and prompts at call time, with no live link, and did not run the child's lifespan or middleware. `mount` is a **live composition** — it holds a live link to the child and runs the child's lifespan and middleware. After switching, later changes to the child become visible through the parent, the child's lifespan runs with the parent's (entered when the server starts, held until it stops — not per request), and the child's middleware runs on the operations delegated to it. If you depended on the frozen-copy behavior (a stable snapshot, no child lifecycle), there is no drop-in replacement: register the child's components on the parent directly instead of composing the two servers. -### Removed parameters and settings +### Removed Parameters Several parameters and settings that warned in 3.x are gone: @@ -221,7 +294,7 @@ Several parameters and settings that warned in 3.x are gone: - **`StreamableHttpTransport(sse_read_timeout=...)`** is removed — it was a no-op under the SDK v2 client. Set the read timeout through the public `Client(transport, timeout=...)` (a `timedelta` or float seconds), or reach for a custom `httpx_client_factory` when you need finer control. (`SSETransport` still accepts `sse_read_timeout`.) - **`ctx.elicit()` now requires `response_type`.** Omitting it (or passing `None`) has warned since 3.2 and now raises `TypeError`. The empty-object schema it produced gave clients nothing to render, and some showed an empty, non-functional form. Pass a type describing what you expect back — `bool` is the right answer for a confirmation: - ```python + ```python test="skip" # Before result = await ctx.elicit("Approve this action?") @@ -231,9 +304,91 @@ Several parameters and settings that warned in 3.x are gone: This is the server-authoring API only. Client elicitation handlers still receive `response_type=None` for URL requests and for empty schemas sent by other servers — that contract is unchanged. -## Behavior changes to verify +### Background Tasks -Three server-side behaviors changed in ways that compile fine but can surface at runtime. +Background tasks left the core MCP spec during the SDK v2 rebuild and came back as the `io.modelcontextprotocol/tasks` extension (SEP-2663). FastMCP follows the protocol: what was a built-in server feature in 3.x is now a registered extension, and the authoring surface changed on both sides of the connection. + +The extension ships in a separate package, so the pin from [Install the v4 Prerelease](#install-the-v4-prerelease) needs one more entry before any of this imports: + +```toml +[project] +dependencies = ["fastmcp[tasks]==4.0.0b1"] + +[tool.uv] +constraint-dependencies = [ + "fastmcp-slim==4.0.0b1", + "fastmcp-tasks==4.0.0b1", + "mcp==2.0.0b2", + "mcp-types==2.0.0b2", +] +``` + +On the server, `task=True` still marks a tool as capable of running in the background, but it no longer runs anything by itself — the extension does. Register it, or the server refuses to start: + +```python +from fastmcp import FastMCP +from fastmcp_tasks import TasksExtension + +mcp = FastMCP("MyServer") +mcp.add_extension(TasksExtension()) + +@mcp.tool(task=True) +async def slow_computation(duration: int) -> str: + """A long-running operation.""" + return "done" +``` + +Without the registration, a `task=True` tool raises at startup rather than the first time a client calls the tool: + +``` +RuntimeError: Task-enabled tools (slow_computation) require the tasks extension, +but no extension with identifier 'io.modelcontextprotocol/tasks' is registered. +``` + +`TaskConfig` moved from `fastmcp.server.tasks` to `fastmcp.utilities.tasks`, and the `CurrentDocket` and `CurrentWorker` dependencies moved to `fastmcp_tasks.dependencies`. + +`task=` is now a tool-only keyword. FastMCP 3 accepted it on resource, resource-template, and prompt decorators as well; passing it to `@mcp.resource` or `@mcp.prompt` now raises `TypeError`, and there is no replacement — the extension tasks tool calls only. + +The client API changed shape entirely. In 3.x you opted a single call into background execution with `task=True` and got a handle back. In 4.0 `call_tool` handles a tasked call transparently: if the server runs the call in the background, the client polls it to completion and returns the same result a synchronous call would have produced. + +```python +import fastmcp_tasks # noqa: F401 — importing anywhere enables client task support +from fastmcp import Client + + +async def run(server): + async with Client(server) as client: + return await client.call_tool("slow_computation", {"duration": 10}) +``` + +When you want the handle — to do other work while the task runs, check on it, or cancel it — `call_tool_task` returns one immediately: + +```python +from fastmcp import Client +from fastmcp_tasks import call_tool_task + + +async def run(server): + async with Client(server) as client: + task = await call_tool_task(client, "slow_computation", {"duration": 10}) + return await task.result() +``` + +Three things follow from this. `client.call_tool(name, args, task=True)` raises `TypeError`, as do `read_resource(task=True)` and `get_prompt(task=True)` — and those last two have no replacement. Client task support requires `fastmcp_tasks` to be imported somewhere in the process, since that import is what makes a `Client` advertise the capability. And tasks are negotiated only on modern connections, so a `mode="legacy"` client never gets them. See [Background Tasks](/servers/tasks) for the full picture. + +## Behavior Changes + +These changes compile fine and can surface at runtime. The first is the one most likely to bite a working 3.x server. + +**`ctx.elicit()` no longer reaches a default client.** Elicitation is era-gated in 4.0: `ctx.elicit()` works on handshake-era connections (≤ 2025-11-25) and raises on the modern `2026-07-28` protocol, which has no back-channel for a running tool to push a request down. Because `fastmcp.Client` now defaults to `mode="auto"`, an ordinary client negotiates the modern era against a FastMCP server — so a tool that elicited happily in 3.x now fails the call: + +``` +ToolError: elicitation via server-initiated requests is unavailable on 2026-07-28 connections. +``` + +The gate is strict in both directions, which is what makes it debuggable: a guard tool that returns an input request on a handshake connection raises the mirror-image error rather than misbehaving quietly. You have three ways forward. Rewrite the tool as a guard tool that *returns* a description of the input it needs, which is the form that works on modern connections. Branch on `ctx.request_context.protocol_version` and keep both paths if you serve both eras. Or keep this server's clients on the handshake era with `Client(server, mode="legacy")`, which leaves `ctx.elicit()` working as written. See [Elicitation](/servers/elicitation#which-approach-to-use) for the two shapes side by side. + +**Middleware sees traffic it never saw before.** Dispatch now begins in the SDK's middleware layer, the single point every inbound message passes through, so `on_message`, `on_request`, and `on_notification` observe *every* message a client sends — including `notifications/cancelled`, `notifications/initialized`, and `notifications/progress`, and including requests that fail before reaching a handler, such as an unknown method or a `tools/call` whose params fail validation. In 3.x those never reached your hooks. Middleware that assumed every message it saw was a routable request, or that counted messages to measure tool traffic, needs a guard on the message type. The operation hooks (`on_call_tool`, `on_list_tools`, and the rest) are unaffected: they still fire exactly once per request and still receive typed component results. See [What middleware sees](/servers/middleware#what-middleware-sees). **Templated resources are path-screened by default.** Every templated resource now has its extracted parameter values checked for path-traversal (`..` segments), absolute paths, and null bytes *before your handler runs*, at the server's read chokepoint. A rejected read returns a non-leaky "resource not found" error. Only a standalone `..` segment counts as traversal, so values that merely contain dots (`file.tar.gz`, `HEAD~3..HEAD`) and dotfiles (`.env`) still pass. If a template legitimately accepts `..`-bearing or absolute values, exempt the parameter with `ResourceSecurity(exempt_params={...})`, disable the check per-component with `security=None`, or set a server-wide default with `FastMCP(resource_security=...)`. See [Resources → Path Security](/servers/resources#path-security). @@ -245,11 +400,11 @@ The cost of the correction is the `iss` on tokens already in the wild, so it fal Servers that leave `issuer_url` unset, or set it to the same value as `base_url`, are unaffected. It defaults to `base_url`, and the metadata and minted `iss` are byte-identical to what 3.x produced. -## Deprecation timeline +## Deprecation Timeline The camelCase bridge is a migration aid, not a permanent fixture. It works today and warns on every bridged read so you can find and update the affected call sites. Plan to migrate your reads to snake_case: the shims will be removed in a future release, after which only the snake_case names resolve — the same state you get today by setting `mcp_camelcase_compat = False`. Turning the setting off is a good way to surface every remaining camelCase read in your code as a hard `AttributeError` before the shims go away. -## SDK deprecation warnings you may see +## SDK Deprecation Warnings Ordinary use of `ctx.info` (client logging) emits an SDK-level `MCPDeprecationWarning`: @@ -259,7 +414,7 @@ The logging capability is deprecated as of 2026-07-28 (SEP-2577) The warning comes from the MCP SDK, not from FastMCP, and it is benign. `ctx.info` and the rest of the logging methods keep working on every era, including the modern one — a log message is a *notification*, which rides the response stream the caller already opened. The SDK is signaling the protocol's direction for the capability declaration, not the notification itself. -## Protocol version support +## Protocol Version Support FastMCP servers built on the SDK v2 serve multiple protocol eras from the same server. The SDK negotiates the era each client speaks: the sessionless `2026-07-28` era (which discovers capabilities through `server/discover`) and earlier session-based handshake versions are all handled simultaneously. This formally supersedes FastMCP's earlier "latest protocol only" stance — a single server now works with clients across the protocol transition. @@ -273,7 +428,7 @@ Migrating differs by capability. For **roots**, the guard pattern is the direct | --- | --- | --- | | `ctx.info` / logging notifications | Supported | Supported | | Tools, resources, prompts, completions | Supported | Supported | -| `ctx.elicit` | Supported | Use the guard pattern (return `InputRequiredResult`) | +| `ctx.elicit` | Supported | Raises — use the guard pattern (return `InputRequiredResult`) | | `ctx.sample` / `ctx.sample_step` | Method removed — call an LLM server-side | Method removed — call an LLM server-side, or ask via the guard pattern | | `ctx.list_roots` | Method removed — take paths as tool arguments | Method removed — ask via the guard pattern, or take paths as tool arguments | | `client.set_logging_level()` | Supported | Raises — `logging/setLevel` needs session state the era lacks | @@ -281,23 +436,25 @@ Migrating differs by capability. For **roots**, the guard pattern is the direct | Session state (`ctx.set_state` across calls) | Persists for the session | Does not persist — every request is a fresh connection | | Background tasks (`task=True`) | Runs synchronously — never tasked | Supported via the tasks extension | -Two of these bite by default now, because **`fastmcp.Client` defaults to `mode="auto"`** in v4 — an ordinary `Client(server)` negotiates the newest protocol both sides share, which against a FastMCP server is the sessionless `2026-07-28` era. On that era there is no `initialize` handshake, so a `Middleware.on_initialize` hook never runs; and each request is a fresh connection, so state written with `ctx.set_state` in one call is not visible in the next. A server that gates access in `on_initialize` or relies on per-session state must keep its clients on the session-based era. The narrow escape is per-client: `Client(server, mode="legacy")`. The durable, server-side answer is to declare the versions the server actually serves so a modern client is refused at connect time rather than silently losing those features — see the server's protocol-version restriction (added alongside this change). +Several of these bite by default now, because **`fastmcp.Client` defaults to `mode="auto"`** in v4 — an ordinary `Client(server)` negotiates the newest protocol both sides share, which against a FastMCP server is the sessionless `2026-07-28` era. On that era there is no `initialize` handshake, so a `Middleware.on_initialize` hook never runs; each request is a fresh connection, so state written with `ctx.set_state` in one call is not visible in the next; and a tool that calls [`ctx.elicit()`](#behavior-changes) raises. A server that gates access in `on_initialize`, relies on per-session state, or elicits mid-tool must keep its clients on the session-based era. The control is per-client: `Client(server, mode="legacy")`. There is no server-side setting that restricts which protocol versions a server offers, so a server whose behavior depends on the handshake era depends on its callers opting into it — which is only practical when you control them. If you don't, port the behavior instead: a guard tool for elicitation, [session state](/servers/sessions) for what `ctx.set_state` held, and per-request auth checks for what `on_initialize` gated. The client side is unaffected. `sampling_handler=` and `roots=` mean what they always did — see [client sampling](/clients/sampling) and [client roots](/clients/roots) — and one registration serves both routes, since a handshake-era server's pushed request and a modern server's returned one dispatch to the same handler. -## Upgrade checklist +## Upgrade Checklist Most servers upgrade untouched. Work down this list to find the ones that don't: 1. **Bump your environment.** Raise any pin below `pydantic>=2.12`; upgrade FastAPI if your resolver complains about Starlette `<1.0.1`. 2. **Fix imports that moved out.** Replace `from mcp.types import X` with `from mcp_types import X`, and update any import from the [removed modules](#moved-imports) (`fastmcp.server.proxy`, `fastmcp.server.openapi`, `fastmcp.server.apps`, the `fastmcp.tools.tool` / `resources.resource` / `prompts.prompt` component shims). -3. **Update removed server APIs.** Swap `as_proxy` → `create_proxy`, `import_server` → `mount`, `mount(prefix=)` → `mount(namespace=)`, and the [other removed methods and keywords](#removed-server-methods-and-mount-keywords). +3. **Update removed server APIs.** Swap `as_proxy` → `create_proxy`, `import_server` → `mount`, `mount(prefix=)` → `mount(namespace=)`, and the [other removed methods and keywords](#removed-server-methods). 4. **Replace `ctx.sample` and `ctx.list_roots`.** Both are gone from `Context`, as are `FastMCP(sampling_handler=...)` and `sampling_handler_behavior=`. Call an LLM directly from your server for generation; ask for roots through the guard pattern, or take file paths as tool arguments. A server whose purpose is to use the caller's model should stay on FastMCP 3.x rather than migrate. -5. **Update removed tool parameters.** Replace tool `serializer=` (return a `ToolResult`), `exclude_args=` (use `Depends()`), and `StreamableHttpTransport(sse_read_timeout=)`. -6. **Fix `McpError` construction.** Positional `McpError(ErrorData(...))` becomes keyword `McpError(code=..., message=...)`. Catching is unchanged. -7. **Move httpx to httpx2.** Grep for `except httpx.` and for custom `httpx_client_factory` / `httpx.Auth` objects handed to FastMCP, and swap the import to `httpx2`. -8. **Decide the client era.** `Client` now defaults to `mode="auto"`. If a server relies on `on_initialize` or per-session state, keep its clients on `mode="legacy"` or restrict the server's served protocol versions. -9. **Verify behavior changes.** Confirm templated resources that legitimately accept `..` or absolute paths are exempted, update any client that matched the old `-32002` resource-not-found code, and if your server mints its own OAuth tokens (`OAuthProxy` and the providers built on it) under an `issuer_url` that differs from its `base_url`, schedule the [one-time re-authorization](#behavior-changes-to-verify) its clients now need. -10. **Run with the camelCase bridge off.** Set `mcp_camelcase_compat = False` (or `FASTMCP_MCP_CAMELCASE_COMPAT=false`) in CI to surface every remaining camelCase read as a hard `AttributeError` before the shims are removed. +5. **Find every `ctx.elicit()` call.** It raises on modern connections, which is what a default client now negotiates. Rewrite the tool as a guard tool, branch on `ctx.request_context.protocol_version`, or keep its clients on `mode="legacy"` — see [the era gate](#behavior-changes). +6. **Register the tasks extension.** A `task=True` tool needs `mcp.add_extension(TasksExtension())` or the server won't start. Drop `task=` from resource and prompt decorators, move `TaskConfig` to `fastmcp.utilities.tasks`, and replace client-side `call_tool(..., task=True)` with plain `call_tool` or `call_tool_task`. +7. **Update removed tool parameters.** Replace tool `serializer=` (return a `ToolResult`), `exclude_args=` (use `Depends()`), and `StreamableHttpTransport(sse_read_timeout=)`. +8. **Fix `McpError` construction.** Positional `McpError(ErrorData(...))` becomes keyword `McpError(code=..., message=...)`. Catching is unchanged. +9. **Move httpx to httpx2.** Grep for `except httpx.` and for custom `httpx_client_factory` / `httpx.Auth` objects handed to FastMCP, and swap the import to `httpx2`. +10. **Decide the client era.** `Client` now defaults to `mode="auto"`. If a server relies on `on_initialize`, per-session state, or `ctx.elicit()`, keep its clients on `mode="legacy"`, or port the behavior forward — there is no server-side protocol-version restriction. +11. **Verify behavior changes.** Confirm templated resources that legitimately accept `..` or absolute paths are exempted, guard any middleware that now sees notifications and unroutable requests, update any client that matched the old `-32002` resource-not-found code, and if your server mints its own OAuth tokens (`OAuthProxy` and the providers built on it) under an `issuer_url` that differs from its `base_url`, schedule the [one-time re-authorization](#behavior-changes) its clients now need. +12. **Run with the camelCase bridge off.** Set `mcp_camelcase_compat = False` (or `FASTMCP_MCP_CAMELCASE_COMPAT=false`) in CI to surface every remaining camelCase read as a hard `AttributeError` before the shims are removed. The executable version of this checklist lives in [`tests/test_upgrade_from_v3.py`](https://github.com/PrefectHQ/fastmcp/blob/main/tests/test_upgrade_from_v3.py): it builds representative 3.x-style servers and asserts they run unchanged, and pins every removed surface to the exact error it now raises. diff --git a/docs/getting-started/upgrading/from-low-level-sdk.mdx b/docs/getting-started/upgrading/from-low-level-sdk-v1.mdx similarity index 61% rename from docs/getting-started/upgrading/from-low-level-sdk.mdx rename to docs/getting-started/upgrading/from-low-level-sdk-v1.mdx index ab49f1574..b7135729a 100644 --- a/docs/getting-started/upgrading/from-low-level-sdk.mdx +++ b/docs/getting-started/upgrading/from-low-level-sdk-v1.mdx @@ -1,7 +1,7 @@ --- -title: Upgrading from the MCP Low-Level SDK -sidebarTitle: "From MCP Low-Level SDK" -description: Upgrade your MCP server from the low-level Python SDK's Server class to FastMCP +title: Upgrading from the Low-Level SDK v1 +sidebarTitle: "From Low-Level SDK v1" +description: Upgrade your MCP server from v1 of the low-level Python SDK's Server class to FastMCP icon: up --- @@ -9,78 +9,90 @@ If you've been building MCP servers directly on the `mcp` package's `Server` cla The core idea: instead of telling the SDK what your tools look like and then separately implementing them, you write ordinary Python functions and let FastMCP derive the protocol layer from your code. Type hints become JSON Schema. Docstrings become descriptions. Return values are serialized automatically. The plumbing you wrote to satisfy the protocol just disappears. -## Why now is the moment to switch +## The SDK v2 Transition -MCP SDK v2 landed sweeping breaking changes on the low-level `Server`: the protocol types moved out of `mcp.types` into a separate `mcp_types` package, every field was renamed from camelCase to snake_case, the `Server` class was rebuilt, `McpError` was renamed, and sessions were removed on the new sessionless protocol era. If you build directly on the low-level SDK, all of that lands on you — you have to rewrite your imports, your handler signatures, and your error construction to match the new surface. +MCP SDK v2 is a substantial, deliberate modernization of the protocol layer. Protocol types moved into a standalone `mcp_types` package, wire fields moved from camelCase to snake_case, and the low-level `Server` was rebuilt so handlers are passed to the constructor as `on_*` callables taking `(ctx, params)` rather than registered with decorators. A v1 server meets that change the moment its environment resolves `mcp` to v2: -Adopting FastMCP is the easier path. FastMCP 4 runs on SDK v2 and hides that entire surface behind a high-level API that did not change. You write `@mcp.tool` and never touch the renamed internals — FastMCP derives the protocol layer from your function signatures, so the SDK v2 rename simply isn't something your code has to know about. Migrating low-level-SDK-v1 code to FastMCP is less work than migrating it to raw SDK v2, and you come out the other side with the whole framework: composition, middleware, proxies, authentication, and testing. The SDK v2 break is the natural moment to make the jump. +``` +ModuleNotFoundError: No module named 'mcp.types' +AttributeError: 'Server' object has no attribute 'list_tools' +``` + +Often nobody chose that moment. An unpinned `mcp` dependency, a fresh lockfile, or a rebuilt container picks up the new major version. Nothing is wrong with your code, and nothing is wrong with the SDK — major versions are exactly where a change like this belongs. Your build just crossed it earlier than you planned to. + +Pinning the SDK back restores the decorator API immediately, with no code changes, and buys you time to choose deliberately: + +```bash +pip install "mcp<2" +``` + +## Two Upgrade Paths + +Both directions are reasonable, and the choice is about which code you'd rather maintain. + +**Porting the low-level `Server` to SDK v2** keeps you in direct control of the protocol surface, which is the point of the low-level API and the right call for some servers. The work is real: your imports, every handler signature, every handler's return type, and your error construction all move. + +**Adopting FastMCP** is what the rest of this page walks through. What makes it less work is not that FastMCP is better — it's that the code most affected by the SDK v2 changes is precisely the code FastMCP doesn't ask you to write. Your `list_tools`/`call_tool` pair, hand-written JSON Schema, and content-block wrappers aren't ported to new signatures; they're deleted, and FastMCP derives all of it from your function signatures instead. FastMCP 4 runs on MCP SDK v2 underneath, so both paths land you on the same modern protocol layer. -Already using FastMCP 1.0 via `from mcp.server.fastmcp import FastMCP`? Your upgrade is simpler — see the [FastMCP 1.0 upgrade guide](/getting-started/upgrading/from-mcp-sdk) instead. +Already on SDK v2's rebuilt `Server` class, with constructor-registered `on_*` handlers? See [Upgrading from the Low-Level SDK v2](/getting-started/upgrading/from-low-level-sdk-v2) instead — the before-and-after code is different enough to warrant its own guide. + +Using FastMCP 1.0 via `from mcp.server.fastmcp import FastMCP`? Your upgrade is a single import — see [Upgrading from MCP SDK v1](/getting-started/upgrading/from-mcp-sdk-v1). -You are upgrading an MCP server from the `mcp` package's low-level Server class (v1) to FastMCP 4. The server currently uses `mcp.server.Server` (or `mcp.server.lowlevel.server.Server`) with manual handler registration. Analyze the provided code and rewrite it using FastMCP's high-level API. The full guide is at https://gofastmcp.com/getting-started/upgrading/from-low-level-sdk and the complete FastMCP documentation is at https://gofastmcp.com — fetch these for complete context. +You are rewriting an MCP server built on v1 of the `mcp` package's low-level `Server` class (`mcp.server.Server` or `mcp.server.lowlevel.server.Server`, with decorator-registered handlers) using FastMCP 4's high-level API. -UPGRADE RULES: +FIRST, fetch https://gofastmcp.com/getting-started/upgrading/from-low-level-sdk-v1 — it explains every item below, with before-and-after code for each handler group. Fetch https://gofastmcp.com for anything the guide doesn't cover. Do not invent a FastMCP API you have not confirmed in the docs. -1. IMPORTS: Replace all `mcp.*` imports with FastMCP equivalents. - - `from mcp.server import Server` or `from mcp.server.lowlevel.server import Server` → `from fastmcp import FastMCP` - - `import mcp.types as types` → remove (not needed for most code) - - `from mcp.server.stdio import stdio_server` → remove (handled by mcp.run()) - - `from mcp.server.sse import SseServerTransport` → remove (handled by mcp.run()) +Then work through the provided code. This is a rewrite, not a patch: most of what you find gets deleted rather than translated. -2. SERVER: Replace `Server("name")` with `FastMCP("name")`. +CONSTRUCTION AND TRANSPORT +- `Server("name")` +- `async with stdio_server() as (r, w): await server.run(r, w, server.create_initialization_options())` +- `SseServerTransport` / `StreamableHTTPSessionManager` and any Starlette wiring around them +- `asyncio.run(main())` boilerplate +- `lifespan=` — carries over directly: pass the same async context manager to `FastMCP(lifespan=...)`, and read what it yields from `ctx.lifespan_context` in any tool. Do not drop it — the tools that depended on it (a DB connection, a client pool) lose their dependency silently if you do. -3. TOOLS: Replace the list_tools + call_tool handler pair with individual @mcp.tool decorators. - - Delete the `@server.list_tools()` handler entirely - - Delete the `@server.call_tool()` handler entirely - - For each tool that was listed in list_tools and dispatched in call_tool, create a new function: - - Decorate it with `@mcp.tool` - - Use the tool name as the function name (or pass name= to the decorator) - - Use the docstring for the description (or pass description= to the decorator) - - Convert the inputSchema JSON Schema into typed Python parameters (e.g., `{"type": "integer"}` → `int`, `{"type": "string"}` → `str`, `{"type": "array", "items": {"type": "string"}}` → `list[str]`) - - Return plain Python values (`str`, `int`, `dict`, etc.) instead of `list[types.TextContent(...)]` - - If the tool returned `types.ImageContent` or `types.EmbeddedResource`, use `from fastmcp.utilities.types import Image` or return the appropriate type +HANDLERS TO DELETE (each becomes one or more decorated functions) +- `@server.list_tools()` + `@server.call_tool()` — note the `if name == ...` dispatch chain inside call_tool; each branch becomes its own `@mcp.tool` +- `@server.list_resources()` + `@server.list_resource_templates()` + `@server.read_resource()` — note any manual URI parsing, which the `{placeholder}` syntax replaces +- `@server.list_prompts()` + `@server.get_prompt()` +- any other `@server.*()` handler in the file — completion, resource subscribe/unsubscribe, logging level, progress. Look these up in the FastMCP docs rather than assuming a decorator name maps one-to-one. -4. RESOURCES: Replace the list_resources + list_resource_templates + read_resource handler trio with individual @mcp.resource decorators. - - Delete all three handlers - - For each static resource, create a function decorated with `@mcp.resource("uri://...")` - - For each resource template, use `@mcp.resource("uri://{param}/path")` with `{param}` in the URI and a matching function parameter - - Return str for text content, bytes for binary content - - Set `mime_type=` in the decorator if needed +TYPES THAT DISAPPEAR FROM YOUR CODE +- hand-written `inputSchema` JSON Schema dicts — these come from type hints now +- `types.Tool`, `types.Resource`, `types.ResourceTemplate`, `types.Prompt`, `types.PromptArgument` +- `types.TextContent` wrappers around return values — return plain Python values instead +- `types.ImageContent`, `types.EmbeddedResource` +- `types.PromptMessage`, `types.GetPromptResult` +- Note that `mcp.types` no longer exists at all in the SDK v2 that FastMCP 4 builds on; any type that genuinely survives the rewrite comes from `mcp_types` now. -5. PROMPTS: Replace the list_prompts + get_prompt handler pair with individual @mcp.prompt decorators. - - Delete both handlers - - For each prompt, create a function decorated with `@mcp.prompt` - - Convert PromptArgument definitions into typed function parameters - - Return str for simple single-message prompts (auto-wrapped as user message) - - Return `list[Message]` for multi-message prompts: `from fastmcp.prompts import Message` - - `Message("text")` defaults to `role="user"`; use `Message("text", role="assistant")` for assistant messages +CONTEXT AND SIDE CHANNELS +- `server.request_context` +- `session.send_log_message(...)`, `session.send_progress_notification(...)` +- direct session use for anything else — a FastMCP `Context` has a `ctx.session` property returning the underlying SDK session, so this still works; prefer a `Context` method where one exists, and note the remaining uses as SDK-coupled -6. TRANSPORT: Replace all transport boilerplate with mcp.run(). - - `async with stdio_server() as (r, w): await server.run(r, w, ...)` → `mcp.run()` (`stdio` is the default) - - SSE/Starlette setup → `mcp.run(transport="sse", host="...", port=...)` - - Streamable HTTP setup → `mcp.run(transport="http", host="...", port=...)` - - Delete asyncio.run(main()) boilerplate — use `if __name__ == "__main__": mcp.run()` +ERRORS +- `raise ValueError(f"Unknown tool: ...")` and other dispatch fallbacks — these become unnecessary +- `McpError` construction and any error-code mapping -7. CONTEXT: Replace `server.request_context` with FastMCP's Context parameter. - - Add `from fastmcp import Context` and add a `ctx: Context` parameter to any tool that needs it - - `server.request_context.session.send_log_message(...)` → `await ctx.info("message")` or `await ctx.warning("message")` - - Progress reporting → `await ctx.report_progress(current, total)` - -For each change, show the original code, explain what it did, and provide the FastMCP equivalent. +For each item found, show the original code, say what it did, and give the FastMCP equivalent. Where several handlers collapse into one decorated function, show the collapse rather than a line-by-line mapping. Call out anything you could not find a documented FastMCP replacement for instead of inventing one. ## Install +FastMCP 4 is in prerelease, so pin the exact version rather than installing unqualified — a bare `pip install fastmcp` or `uv add fastmcp` resolves to the latest *stable* release, which today is FastMCP 3: + ```bash -pip install --upgrade fastmcp +pip install "fastmcp==4.0.0b1" # or -uv add fastmcp +uv add "fastmcp==4.0.0b1" ``` -FastMCP includes the `mcp` package as a transitive dependency, so you don't lose access to anything. +An exact version pin installs even though it's a prerelease — neither installer needs `--pre` or `--prerelease allow` for a version this specific, only for an open-ended range. For a reproducible lockfile that also pins the prerelease protocol dependencies, see [Install the v4 Prerelease](/getting-started/upgrading/from-fastmcp-3#install-the-v4-prerelease). + +FastMCP depends on the `mcp` package, so the SDK stays installed. Note that FastMCP 4 builds on SDK v2, where `mcp.types` no longer exists — protocol types live in the standalone `mcp_types` package now. Most of your `mcp.types` imports disappear entirely in the rewrite below, since FastMCP derives the protocol types from your function signatures. For the few you still need, import them from `mcp_types`. ## Server and Transport @@ -88,7 +100,7 @@ The `Server` class requires you to choose a transport, connect streams, build in -```python Before +```python Before test="skip" import asyncio from mcp.server import Server from mcp.server.stdio import stdio_server @@ -124,7 +136,12 @@ if __name__ == "__main__": Need HTTP instead of stdio? With the `Server` class, you'd wire up Starlette routes and `SseServerTransport` or `StreamableHTTPSessionManager`. With FastMCP: ```python -mcp.run(transport="http", host="0.0.0.0", port=8000) +from fastmcp import FastMCP + +mcp = FastMCP("my-server") + +if __name__ == "__main__": + mcp.run(transport="http", host="0.0.0.0", port=8000) ``` ## Tools @@ -133,7 +150,7 @@ This is where the difference is most dramatic. The `Server` class requires two h -```python Before +```python Before test="skip" import mcp.types as types from mcp.server import Server @@ -240,7 +257,7 @@ The `Server` class uses three handlers for resources: `list_resources()` to enum -```python Before +```python Before test="skip" import json import mcp.types as types from mcp.server import Server @@ -333,7 +350,7 @@ Same pattern: the `Server` class uses `list_prompts()` and `get_prompt()` with m -```python Before +```python Before test="skip" import mcp.types as types from mcp.server import Server @@ -420,7 +437,7 @@ The `Server` class exposes request context through `server.request_context`, whi -```python Before +```python Before test="skip" import mcp.types as types from mcp.server import Server @@ -458,13 +475,31 @@ async def process_data(ctx: Context) -> str: The `Context` object provides logging (`ctx.debug()`, `ctx.info()`, `ctx.warning()`, `ctx.error()`), progress reporting (`ctx.report_progress()`), resource subscriptions, session state, and more. See [Context](/servers/context) for the full API. +## Errors + +Most of the errors a low-level server raises disappear with the dispatch that raised them: the `ValueError(f"Unknown tool: {name}")` fallback is unnecessary once FastMCP routes calls, and an exception from your function body is converted to a tool error for you. + +Deliberate protocol errors are the exception, and they need a small rewrite. The v1 pattern wrapped an `ErrorData` and passed it positionally; FastMCP's `McpError` takes the fields directly: + +```python test="skip" +from fastmcp.exceptions import McpError + +# Before (SDK v1): +# raise McpError(ErrorData(code=-32000, message="Upstream unavailable")) + +# After: +raise McpError(code=-32000, message="Upstream unavailable") +``` + +An optional third argument, `data=`, carries the structured payload `ErrorData` used to hold. Catching is unchanged — `except McpError` still works, and `err.error.code` still reads the code — so only construction sites need touching. + ## Complete Example A full server upgrade, showing how all the pieces fit together: -```python Before expandable +```python Before expandable test="skip" import asyncio import json import mcp.types as types @@ -580,15 +615,10 @@ if __name__ == "__main__": -## What's Next +## What You Gain -Once you've upgraded, you have access to everything FastMCP provides beyond the basics: +Deleting the handler machinery is the immediate payoff, but the reason to make this move is what becomes available once your server is a FastMCP server. -- **[Server composition](/servers/composition)** — Mount sub-servers to build modular applications -- **[Middleware](/servers/middleware)** — Add logging, rate limiting, error handling, and caching -- **[Proxy servers](/servers/providers/proxy)** — Create a proxy to any existing MCP server -- **[OpenAPI integration](/integrations/openapi)** — Generate an MCP server from an OpenAPI spec -- **[Authentication](/servers/auth/authentication)** — Built-in OAuth and token verification -- **[Testing](/servers/testing)** — Test your server directly in Python without running a subprocess +[Server composition](/servers/composition) mounts one server inside another, so a surface that grew unwieldy as a single `call_tool` dispatch splits into modules developed and tested independently. [Middleware](/servers/middleware) runs across every request for logging, rate limiting, error handling, and caching — the cross-cutting concerns that, on the low-level `Server`, meant threading the same code through every handler. [Proxy servers](/servers/providers/proxy) put a FastMCP server in front of any existing MCP server, bridging transports and adding auth to a backend you don't control, and the [OpenAPI integration](/integrations/openapi) generates an entire server from an API specification you already have. [Authentication](/servers/auth/authentication) arrives as a single `auth=` provider covering token verification, OAuth, and named providers for GitHub, Google, Auth0, and others. -Explore the full documentation at [gofastmcp.com](https://gofastmcp.com). +The change most likely to affect your daily work is [testing](/servers/testing). FastMCP ships a client that connects to a server object in the same Python process, so a test calls your tools directly — no subprocess, no stdio pipes, no transport to stand up. diff --git a/docs/getting-started/upgrading/from-low-level-sdk-v2.mdx b/docs/getting-started/upgrading/from-low-level-sdk-v2.mdx new file mode 100644 index 000000000..f4222c0f0 --- /dev/null +++ b/docs/getting-started/upgrading/from-low-level-sdk-v2.mdx @@ -0,0 +1,622 @@ +--- +title: Upgrading from the Low-Level SDK v2 +sidebarTitle: "From Low-Level SDK v2" +description: Move a server built on v2 of the low-level Python SDK's Server class to FastMCP +icon: up +--- + +If your server builds on the `mcp` package's low-level `Server` class as SDK v2 rebuilt it — handlers passed to the constructor as `on_list_tools`, `on_call_tool`, and their siblings, each taking `(ctx, params)` and returning a wrapped result object — this guide is for you. FastMCP replaces that machinery with a declarative API where your functions *are* the protocol surface. + +The core idea: instead of describing your tools to the SDK and then separately implementing them, you write ordinary Python functions and let FastMCP derive the protocol layer from your code. Type hints become JSON Schema. Docstrings become descriptions. Return values are serialized automatically. The dispatch you wrote to route a call by name, and the schemas you wrote by hand to describe it, both disappear. + +Migrating from SDK v2 is the most direct of the four upgrade paths, because you and FastMCP already share a protocol layer. FastMCP 4 is built on SDK v2, so `mcp_types` imports keep working, field names are already snake_case, and the era negotiation you get is the one you have. Almost nothing about the wire changes — the one exception is [argument strictness](#stricter-arguments), covered below. + + +On SDK v1's decorator-registered `Server` — `@server.list_tools()`, `@server.call_tool()` — instead? See [Upgrading from the Low-Level SDK v1](/getting-started/upgrading/from-low-level-sdk-v1), where the before-and-after code matches that API. + +Using SDK v2's high-level `MCPServer` class? See [Upgrading from MCP SDK v2](/getting-started/upgrading/from-mcp-sdk-v2) — that migration is mostly renaming. + + + +You are rewriting an MCP server built on the MCP Python SDK v2's low-level `Server` class (`mcp.server.lowlevel.server.Server`, with `on_*` handlers passed to the constructor) using FastMCP 4's high-level API. + +FIRST, fetch https://gofastmcp.com/getting-started/upgrading/from-low-level-sdk-v2 — it explains every item below in full, with before-and-after code. Fetch https://gofastmcp.com for anything the guide doesn't cover. Do not guess at a FastMCP API you have not confirmed in the docs. + +Then work through the provided code looking for each of these. The guide has the replacement for every one: + +CONSTRUCTION AND TRANSPORT +- `Server(name, on_list_tools=..., on_call_tool=..., ...)` — the whole constructor, including every handler passed to it +- `server.run(read_stream, write_stream, server.create_initialization_options())` and its `stdio_server()` context manager +- `server.streamable_http_app()` and any Starlette app assembled around it +- `asyncio.run(main())` boilerplate +- `lifespan=` — carries over directly: pass the same async context manager to `FastMCP(lifespan=...)`, and read what it yields from `ctx.lifespan_context` in any tool. Do not drop it — the tools that depended on it (a DB connection, a client pool) lose their dependency silently if you do. + +HANDLERS TO DELETE, EACH REPLACED BY ONE DECORATOR (not simply removed) +- `on_list_tools` + `on_call_tool` → one `@mcp.tool` function per branch of the `if params.name == ...` dispatch chain inside `on_call_tool` +- `on_list_resources` + `on_list_resource_templates` + `on_read_resource` → one `@mcp.resource` function per resource/template +- `on_list_prompts` + `on_get_prompt` → one `@mcp.prompt` function per prompt +- `on_completion` → one `@mcp.completion` function. This one is easy to drop by mistake: skipping it does not just remove autocomplete cleanly, it silently stops FastMCP from advertising the completions capability at all, since that capability is only advertised when a handler is registered. +- `on_subscribe_resource` / `on_unsubscribe_resource` / `on_subscriptions_listen` — flag for the user, no single-decorator equivalent +- `on_set_logging_level`, `on_progress`, `on_roots_list_changed`, `on_ping` — flag for the user, these are protocol-level hooks with no direct FastMCP surface + +TYPES THAT DISAPPEAR FROM YOUR CODE +- Hand-written `input_schema` / `output_schema` JSON Schema dicts — these come from type hints now +- `types.ListToolsResult`, `types.CallToolResult`, `types.ListResourcesResult`, `types.ListResourceTemplatesResult`, `types.ReadResourceResult`, `types.ListPromptsResult`, `types.GetPromptResult` — result wrappers FastMCP builds for you +- `types.TextContent`, `types.TextResourceContents`, `types.BlobResourceContents` — return plain Python values instead +- `types.ImageContent` / `types.AudioContent` — `fastmcp.utilities.types.Image` / `Audio` +- `types.Tool`, `types.Resource`, `types.ResourceTemplate`, `types.Prompt`, `types.PromptArgument` — declaration types FastMCP derives +- `types.PromptMessage` — `fastmcp.prompts.Message` +- Note which `mcp_types` imports are still needed afterward; protocol types are unchanged in FastMCP, so surviving imports stay as they are. + +CONTEXT AND SIDE CHANNELS +- `ctx.session.send_log_message(...)` — `ctx.info()` / `ctx.debug()` / `ctx.warning()` / `ctx.error()` on a `fastmcp.Context` parameter +- `ctx.session.report_progress(...)` — `ctx.report_progress()` +- `ctx.request_id`, `ctx.meta`, `ctx.protocol_version` — these live on `ctx.request_context` in FastMCP (`ctx.request_context.request_id`, and so on); note that `ctx.protocol_version` directly on the Context does not exist +- `ctx.params` — no equivalent, and none is needed: the raw request params were how a low-level handler read the tool's arguments, and those are now the decorated function's typed parameters. `ctx.request_context.params` does NOT exist and raises AttributeError. +- Direct `ctx.session` use for anything else — `Context.session` exists in FastMCP too and returns the same raw SDK session, so this still works; prefer a `Context` method where one exists, and note the remaining uses as SDK-coupled + +ERRORS AND AUTH +- `raise ValueError(f"Unknown tool: ...")` dispatch fallbacks — these become unnecessary +- `MCPError` construction and any error-code mapping +- `auth=AuthSettings(...)`, `token_verifier=`, `auth_server_provider=` — one `auth=` provider in FastMCP +- `TransportSecuritySettings` + +For each item found, show the original code, say what it did, and give the FastMCP equivalent. Where several handlers collapse into one decorated function, show the collapse rather than a line-by-line mapping. Call out anything you could not find a documented FastMCP replacement for instead of inventing one. + + +## Install + +FastMCP 4 is in prerelease, so pin the exact version rather than installing unqualified — a bare `pip install fastmcp` or `uv add fastmcp` resolves to the latest *stable* release, which today is FastMCP 3: + +```bash +pip install "fastmcp==4.0.0b1" +# or +uv add "fastmcp==4.0.0b1" +``` + +An exact version pin installs even though it's a prerelease — neither installer needs `--pre` or `--prerelease allow` for a version this specific, only for an open-ended range. For a reproducible lockfile that also pins the prerelease protocol dependencies, see [Install the v4 Prerelease](/getting-started/upgrading/from-fastmcp-3#install-the-v4-prerelease). + +FastMCP 4 depends on the MCP SDK v2 you are already using, so `mcp_types` stays importable and every protocol type keeps its current name and fields. Most of those imports vanish from your code anyway — FastMCP derives them — but the ones you keep need no changes. + +## Server and Transport + +The `Server` class asks you to open a transport, connect its streams, build initialization options, and run an event loop. FastMCP collapses that into a constructor and a `run()` call. + + + +```python Before test="skip" +import asyncio + +from mcp.server.lowlevel.server import Server +from mcp.server.stdio import stdio_server + +server = Server("my-server") # plus every on_* handler + +async def main(): + async with stdio_server() as (read_stream, write_stream): + await server.run( + read_stream, + write_stream, + server.create_initialization_options(), + ) + +asyncio.run(main()) +``` + +```python After +from fastmcp import FastMCP + +mcp = FastMCP("my-server") + +# ... register tools, resources, prompts ... + +if __name__ == "__main__": + mcp.run() +``` + + + +Serving HTTP is the same shape. Where the low-level class hands you a Starlette app from `server.streamable_http_app()` and leaves the hosting to you, FastMCP runs it directly: + +```python +from fastmcp import FastMCP + +mcp = FastMCP("my-server") + +if __name__ == "__main__": + mcp.run(transport="http", host="0.0.0.0", port=8000) +``` + +`mcp.http_app()` still returns a Starlette app when you need to mount the server inside a larger application. + +## Tools + +This is where the difference is largest. SDK v2 requires two handlers — one describing your tools with hand-written JSON Schema, one dispatching calls by name — and both are passed to the constructor, so the connection between a tool's declaration and its implementation lives only in your head. FastMCP derives both from the function. + + + +```python Before +import mcp_types as types +from mcp.server.context import ServerRequestContext +from mcp.server.lowlevel.server import Server + + +async def list_tools(ctx: ServerRequestContext, params) -> types.ListToolsResult: + number = {"type": "number"} + schema = { + "type": "object", + "properties": {"a": number, "b": number}, + "required": ["a", "b"], + } + return types.ListToolsResult( + tools=[ + types.Tool(name="add", description="Add two numbers", input_schema=schema), + types.Tool( + name="multiply", description="Multiply two numbers", input_schema=schema + ), + ] + ) + + +async def call_tool( + ctx: ServerRequestContext, params: types.CallToolRequestParams +) -> types.CallToolResult: + arguments = params.arguments or {} + if params.name == "add": + result = arguments["a"] + arguments["b"] + elif params.name == "multiply": + result = arguments["a"] * arguments["b"] + else: + raise ValueError(f"Unknown tool: {params.name}") + return types.CallToolResult(content=[types.TextContent(type="text", text=str(result))]) + + +server = Server("math", on_list_tools=list_tools, on_call_tool=call_tool) +``` + +```python After +from fastmcp import FastMCP + +mcp = FastMCP("math") + + +@mcp.tool +def add(a: float, b: float) -> float: + """Add two numbers""" + return a + b + + +@mcp.tool +def multiply(a: float, b: float) -> float: + """Multiply two numbers""" + return a * b +``` + + + +Each `@mcp.tool` function is self-contained: its name becomes the tool name, its docstring becomes the description, its annotations become the JSON Schema, and its return value is serialized for you. The dispatch chain, the schema dicts, the `CallToolResult` wrapper, the `TextContent` wrapper, and the unknown-tool fallback all go away — a tool that doesn't exist is now the framework's problem, not a branch you maintain. + +### Type Mapping + +Your hand-written `input_schema` becomes the function's parameters: + +| JSON Schema | Python type | +|---|---| +| `{"type": "string"}` | `str` | +| `{"type": "number"}` | `float` | +| `{"type": "integer"}` | `int` | +| `{"type": "boolean"}` | `bool` | +| `{"type": "array", "items": {"type": "string"}}` | `list[str]` | +| `{"type": "object"}` | `dict` | +| A property absent from `required` | `param: str \| None = None` | + +Constraints carry over too. A schema with `"minimum"` and `"maximum"` becomes a Pydantic `Field`, and a nested object schema becomes a Pydantic model or dataclass used as the annotation — FastMCP generates the same schema back out of it. + +### Return Values + +The low-level class requires tools to return a `CallToolResult` wrapping a list of content blocks. FastMCP takes the value itself — strings, numbers, dicts, lists, dataclasses, Pydantic models — and handles both the content block and the structured output. For images and audio, FastMCP provides wrapper types that carry the format: + +```python +from fastmcp import FastMCP +from fastmcp.utilities.types import Image + +mcp = FastMCP("media") + + +@mcp.tool +def create_chart(data: list[float]) -> Image: + """Generate a chart from data.""" + png_bytes = render_png(data) # your logic + return Image(data=png_bytes, format="png") +``` + +When you need full control over the wire result — multiple content blocks, or structured content that differs from the content blocks — return a `ToolResult` from `fastmcp.tools` instead. + +### Stricter Arguments + +Deriving the schema from your signature also tightens what callers may send, and this is the one behavior change the migration introduces. Your `on_call_tool` handler reads `params.arguments` as a plain dict and never looks at keys it doesn't need, so a call carrying an unexpected key succeeds. FastMCP declares `"additionalProperties": false` on the generated schema and enforces it, so the same call fails: + +```python test="skip" +# Against the low-level handler: succeeds, "extra" never read. +# Against FastMCP: raises, "extra" is not a parameter of greet(). +await client.call_tool("greet", {"name": "World", "extra": "surprise"}) +``` + +For most servers this is an improvement that costs nothing — a caller sending keys your handler never read was already a bug, and the hand-written schema never advertised that they were allowed. It matters if a client in your fleet attaches metadata alongside real arguments, since those calls start failing the moment you migrate. Accept them explicitly as optional parameters if you need to keep them working. + +## Resources + +Resources take three handlers on the low-level class: one to list static resources, one to list URI templates, and one to read whichever URI arrives, with routing you write by hand. FastMCP replaces all three with a decorator per resource, and detects templates from the URI itself. + + + +```python Before +import json + +import mcp_types as types +from mcp.server.context import ServerRequestContext +from mcp.server.lowlevel.server import Server + + +async def list_resources(ctx: ServerRequestContext, params) -> types.ListResourcesResult: + return types.ListResourcesResult( + resources=[ + types.Resource( + uri="config://app", + name="app_config", + description="Application configuration", + mime_type="application/json", + ) + ] + ) + + +async def list_resource_templates( + ctx: ServerRequestContext, params +) -> types.ListResourceTemplatesResult: + return types.ListResourceTemplatesResult( + resource_templates=[ + types.ResourceTemplate( + uri_template="users://{user_id}/profile", + name="user_profile", + description="User profile by ID", + ) + ] + ) + + +async def read_resource( + ctx: ServerRequestContext, params: types.ReadResourceRequestParams +) -> types.ReadResourceResult: + uri = str(params.uri) + if uri == "config://app": + text = json.dumps({"debug": False, "version": "1.0"}) + elif uri.startswith("users://"): + user_id = uri.split("/")[2] + text = json.dumps({"id": user_id, "name": f"User {user_id}"}) + else: + raise ValueError(f"Unknown resource: {uri}") + return types.ReadResourceResult( + contents=[ + types.TextResourceContents( + uri=params.uri, mime_type="application/json", text=text + ) + ] + ) + + +server = Server( + "data", + on_list_resources=list_resources, + on_list_resource_templates=list_resource_templates, + on_read_resource=read_resource, +) +``` + +```python After +import json + +from fastmcp import FastMCP + +mcp = FastMCP("data") + + +@mcp.resource("config://app", mime_type="application/json") +def app_config() -> str: + """Application configuration""" + return json.dumps({"debug": False, "version": "1.0"}) + + +@mcp.resource("users://{user_id}/profile", mime_type="application/json") +def user_profile(user_id: str) -> str: + """User profile by ID""" + return json.dumps({"id": user_id, "name": f"User {user_id}"}) +``` + + + +The URI does the routing. A `{placeholder}` in the URI makes the resource a template, and FastMCP matches the parameter to the function argument of the same name — so the `uri.split("/")[2]` parsing goes away along with the handler that held it. Return a `str` for text content and `bytes` for binary; FastMCP builds the `TextResourceContents` or `BlobResourceContents` wrapper. + +Templated resources also gain a protection the low-level version left to you: FastMCP screens extracted parameter values for path traversal, absolute paths, and null bytes before your function runs. See [Path Security](/servers/resources#path-security) if a template legitimately accepts those values. + +## Prompts + +The same collapse, one more time: `on_list_prompts` declares arguments as `PromptArgument` objects, `on_get_prompt` routes by name and assembles a `GetPromptResult` of `PromptMessage` objects. FastMCP takes a function whose parameters are the arguments. + + + +```python Before +import mcp_types as types +from mcp.server.context import ServerRequestContext +from mcp.server.lowlevel.server import Server + + +async def list_prompts(ctx: ServerRequestContext, params) -> types.ListPromptsResult: + return types.ListPromptsResult( + prompts=[ + types.Prompt( + name="review_code", + description="Review code for issues", + arguments=[ + types.PromptArgument( + name="code", description="The code to review", required=True + ), + types.PromptArgument( + name="language", description="Programming language", required=False + ), + ], + ) + ] + ) + + +async def get_prompt( + ctx: ServerRequestContext, params: types.GetPromptRequestParams +) -> types.GetPromptResult: + if params.name != "review_code": + raise ValueError(f"Unknown prompt: {params.name}") + arguments = params.arguments or {} + language = arguments.get("language", "") + note = f" (written in {language})" if language else "" + text = f"Please review this code{note}:\n\n{arguments.get('code', '')}" + return types.GetPromptResult( + description="Code review prompt", + messages=[ + types.PromptMessage( + role="user", content=types.TextContent(type="text", text=text) + ) + ], + ) + + +server = Server("prompts", on_list_prompts=list_prompts, on_get_prompt=get_prompt) +``` + +```python After +from fastmcp import FastMCP + +mcp = FastMCP("prompts") + + +@mcp.prompt +def review_code(code: str, language: str | None = None) -> str: + """Review code for issues""" + note = f" (written in {language})" if language else "" + return f"Please review this code{note}:\n\n{code}" +``` + + + +Returning a `str` wraps it as a single user message. Whether an argument is required is read from the signature: `code` has no default, so it's required; `language` defaults to `None`, so it isn't. Multi-turn prompts return a list of `Message` objects, which take their text positionally and default to the user role: + +```python +from fastmcp import FastMCP +from fastmcp.prompts import Message + +mcp = FastMCP("prompts") + + +@mcp.prompt +def debug_session(error: str) -> list[Message]: + """Start a debugging conversation""" + return [ + Message(f"I'm seeing this error:\n\n{error}"), + Message("I'll help you debug that. Can you share the relevant code?", role="assistant"), + ] +``` + +## Request Context + +The low-level class hands each handler a `ServerRequestContext` carrying the raw `ServerSession`, and you reach through it to send notifications. FastMCP injects a typed `Context` into any function that declares one, and puts the operations you actually want on it directly. + + + +```python Before +import mcp_types as types +from mcp.server.context import ServerRequestContext +from mcp.server.lowlevel.server import Server + + +async def call_tool( + ctx: ServerRequestContext, params: types.CallToolRequestParams +) -> types.CallToolResult: + if params.name == "process_data": + await ctx.session.send_log_message(level="info", data="Starting processing...") + await ctx.session.report_progress(1, 2) + # ... do work ... + await ctx.session.send_log_message(level="info", data="Done!") + return types.CallToolResult( + content=[types.TextContent(type="text", text="Processed")] + ) + raise ValueError(f"Unknown tool: {params.name}") + + +server = Server("worker", on_call_tool=call_tool) +``` + +```python After +from fastmcp import FastMCP, Context + +mcp = FastMCP("worker") + + +@mcp.tool +async def process_data(ctx: Context) -> str: + """Process data with progress logging""" + await ctx.info("Starting processing...") + await ctx.report_progress(1, 2) + # ... do work ... + await ctx.info("Done!") + return "Processed" +``` + + + +The `Context` parameter is injected by type annotation and never appears in the tool's schema, so clients see `process_data` as taking no arguments. Beyond logging and progress, it carries resource reads, [session state](/servers/sessions), elicitation, and component visibility — see [Context](/servers/context) for the full surface. + +One thing to check as you migrate: `ctx.session` still exists on a FastMCP `Context` as an escape hatch, and it hands back the same raw SDK session your handlers use today. That makes it a working translation for anything with no `Context` equivalent — but it's also the one part of your server that stays coupled to SDK internals, so reach for the `Context` method first and keep the escape hatch for what genuinely has no equivalent. + +## Complete Example + +Everything above, applied at once: + + + +```python Before expandable +import json + +import mcp_types as types +from mcp.server.context import ServerRequestContext +from mcp.server.lowlevel.server import Server + + +async def list_tools(ctx: ServerRequestContext, params) -> types.ListToolsResult: + return types.ListToolsResult( + tools=[ + types.Tool( + name="greet", + description="Greet someone by name", + input_schema={ + "type": "object", + "properties": {"name": {"type": "string"}}, + "required": ["name"], + }, + ) + ] + ) + + +async def call_tool( + ctx: ServerRequestContext, params: types.CallToolRequestParams +) -> types.CallToolResult: + if params.name == "greet": + name = (params.arguments or {})["name"] + return types.CallToolResult( + content=[types.TextContent(type="text", text=f"Hello, {name}!")] + ) + raise ValueError(f"Unknown tool: {params.name}") + + +async def list_resources(ctx: ServerRequestContext, params) -> types.ListResourcesResult: + return types.ListResourcesResult( + resources=[ + types.Resource( + uri="info://version", name="version", description="Server version" + ) + ] + ) + + +async def read_resource( + ctx: ServerRequestContext, params: types.ReadResourceRequestParams +) -> types.ReadResourceResult: + if str(params.uri) != "info://version": + raise ValueError(f"Unknown resource: {params.uri}") + return types.ReadResourceResult( + contents=[ + types.TextResourceContents( + uri=params.uri, text=json.dumps({"version": "1.0.0"}) + ) + ] + ) + + +async def list_prompts(ctx: ServerRequestContext, params) -> types.ListPromptsResult: + return types.ListPromptsResult( + prompts=[ + types.Prompt( + name="summarize", + description="Summarize text", + arguments=[types.PromptArgument(name="text", required=True)], + ) + ] + ) + + +async def get_prompt( + ctx: ServerRequestContext, params: types.GetPromptRequestParams +) -> types.GetPromptResult: + if params.name != "summarize": + raise ValueError(f"Unknown prompt: {params.name}") + text = (params.arguments or {}).get("text", "") + return types.GetPromptResult( + description="Summarize text", + messages=[ + types.PromptMessage( + role="user", + content=types.TextContent(type="text", text=f"Summarize:\n\n{text}"), + ) + ], + ) + + +server = Server( + "demo", + on_list_tools=list_tools, + on_call_tool=call_tool, + on_list_resources=list_resources, + on_read_resource=read_resource, + on_list_prompts=list_prompts, + on_get_prompt=get_prompt, +) +``` + +```python After +import json + +from fastmcp import FastMCP + +mcp = FastMCP("demo") + + +@mcp.tool +def greet(name: str) -> str: + """Greet someone by name""" + return f"Hello, {name}!" + + +@mcp.resource("info://version") +def version() -> str: + """Server version""" + return json.dumps({"version": "1.0.0"}) + + +@mcp.prompt +def summarize(text: str) -> str: + """Summarize text""" + return f"Summarize:\n\n{text}" + + +if __name__ == "__main__": + mcp.run() +``` + + + +## What You Gain + +Deleting the handler machinery is the immediate payoff, but the reason to make this move is what becomes available once your server is a FastMCP server. + +[Server composition](/servers/composition) mounts one server inside another, so a surface that grew unwieldy as a single dispatch chain splits into modules developed and tested independently. [Middleware](/servers/middleware) runs across every request for logging, rate limiting, error handling, and caching, with hooks at whichever level of specificity you need — the cross-cutting concerns that, on the low-level class, meant threading the same code through every handler. [Proxy servers](/servers/providers/proxy) put a FastMCP server in front of any existing MCP server, bridging transports and adding auth to a backend you don't control, and the [OpenAPI integration](/integrations/openapi) generates an entire server from an API specification you already have. [Authentication](/servers/auth/authentication) consolidates the SDK's separate token verifier, authorization-server provider, and `AuthSettings` into a single `auth=` provider, with named providers for GitHub, Google, Auth0, Keycloak, and others. + +The change most likely to affect your daily work is [testing](/servers/testing). FastMCP ships a client that connects to a server object in the same Python process, so a test calls your tools directly — no subprocess, no stdio pipes, no transport to stand up. diff --git a/docs/getting-started/upgrading/from-mcp-sdk-v1.mdx b/docs/getting-started/upgrading/from-mcp-sdk-v1.mdx new file mode 100644 index 000000000..3e08cd100 --- /dev/null +++ b/docs/getting-started/upgrading/from-mcp-sdk-v1.mdx @@ -0,0 +1,264 @@ +--- +title: Upgrading from MCP SDK v1 +sidebarTitle: "From MCP SDK v1" +description: Upgrade from FastMCP 1.0, bundled in v1 of the MCP Python SDK, to the standalone FastMCP framework +icon: up +--- + +If your server starts with `from mcp.server.fastmcp import FastMCP`, you're using FastMCP 1.0 — the version bundled with v1 of the `mcp` package. Upgrading to the standalone FastMCP framework is easy. **For most servers, it's a single import change.** + +```python test="skip" +# Before +from mcp.server.fastmcp import FastMCP + +# After +from fastmcp import FastMCP +``` + +That's it. Your `@mcp.tool`, `@mcp.resource`, and `@mcp.prompt` decorators, your `mcp.run()` call, and the rest of your server code all work as-is. + + +**Why upgrade?** FastMCP 1.0 pioneered the Pythonic MCP server experience, and we're proud it was bundled into the `mcp` package. The standalone FastMCP project has since grown into a full framework for taking MCP servers from prototype to production — with composition, middleware, proxy servers, authentication, and much more. Upgrading gives you access to all of that, plus ongoing updates and fixes. + + +## The SDK v2 Transition + +MCP SDK v2 is a substantial, deliberate modernization of the protocol layer, and part of that work rebuilt the high-level server as `MCPServer` under `mcp.server.mcpserver`. `mcp.server.fastmcp` does not exist there — so a FastMCP 1.0 server meets the change the moment its environment resolves `mcp` to v2: + +``` +ModuleNotFoundError: No module named 'mcp.server.fastmcp' +``` + +Often nobody chose that moment. An unpinned `mcp` dependency, a fresh lockfile, or a rebuilt container picks up the new major version and the module your server imports on line one has moved. Nothing is wrong with your code, and nothing is wrong with the SDK — major versions are exactly where a change like this belongs. Your build just crossed it earlier than you planned to. + +Pinning the SDK back restores the old module immediately, with no code changes, and buys you time to choose deliberately: + +```bash +pip install "mcp<2" +``` + +## Two Upgrade Paths + +From here, both directions are reasonable, and which is less work depends on which API you already write. + +**`MCPServer`, the SDK's high-level server**, is a capable, well-designed API and the direct continuation of the SDK's own line. Because it was rebuilt rather than renamed, expect real work: a new class and import, a different decorator call style, and protocol types imported from the standalone `mcp_types` package with snake_case field names. + +**FastMCP** is the import change at the top of this page. It is short for a specific, historical reason: FastMCP 1.0 *is* early FastMCP — it was contributed into the `mcp` package, and the standalone project kept developing that same high-level API. The surface you already write against is the surface FastMCP still offers. FastMCP 4 is itself built on MCP SDK v2, so both paths land you on the same modern protocol layer; FastMCP absorbs the adaptation internally rather than asking your code to do it. + +The claim is narrower than it may sound. It holds for FastMCP 1.0 servers specifically, because of shared lineage — not because one library is better than the other. Both projects are moving the same direction on the same protocol. + +If you have already moved to SDK v2 and write against `MCPServer` today, see [Upgrading from MCP SDK v2](/getting-started/upgrading/from-mcp-sdk-v2). If your server uses the low-level `Server` class rather than the high-level one, see [Upgrading from the Low-Level SDK v1](/getting-started/upgrading/from-low-level-sdk-v1). + +## Install + +FastMCP 4 is in prerelease, so pin the exact version rather than installing unqualified — a bare `pip install fastmcp` or `uv add fastmcp` resolves to the latest *stable* release, which today is FastMCP 3: + +```bash +pip install "fastmcp==4.0.0b1" +# or +uv add "fastmcp==4.0.0b1" +``` + +An exact version pin installs even though it's a prerelease — neither installer needs `--pre` or `--prerelease allow` for a version this specific, only for an open-ended range. For a reproducible lockfile that also pins the prerelease protocol dependencies, see [Install the v4 Prerelease](/getting-started/upgrading/from-fastmcp-3#install-the-v4-prerelease). + +FastMCP depends on the `mcp` package, so the SDK stays installed and importable. What changes is which parts of it you reach for. FastMCP 4 builds on SDK v2, where `mcp.server.fastmcp` and `mcp.types` are both gone — anything you imported from those two modules needs a new home, and the sections below cover both. Update your import, run your server, and if your tools work, you're done. + + +You are upgrading an MCP server from FastMCP 1.0 (bundled in v1 of the `mcp` package) to standalone FastMCP 4. + +FIRST, fetch https://gofastmcp.com/getting-started/upgrading/from-mcp-sdk-v1 — it explains every item below, with the replacement code. Fetch https://gofastmcp.com for anything the guide doesn't cover. Do not invent a FastMCP API you have not confirmed in the docs. + +For most servers the entire upgrade is the first item. Work through the rest looking for signals, and report only what you actually find. + +THE IMPORT (every server needs this) +- `from mcp.server.fastmcp import FastMCP` → `from fastmcp import FastMCP` +- `from mcp.server.fastmcp import Context` +- `from mcp.server.fastmcp import Image` + +CONSTRUCTOR ARGUMENTS THAT MOVED (all raise TypeError) +- moved to run()/http_app(), and FastMCP names them in the error: host, port, log_level, debug, sse_path, message_path, streamable_http_path, json_response, stateless_http +- moved but rejected with only a generic "unexpected keyword argument", so flag these explicitly: `event_store=` (→ `http_app(event_store=...)`; dropping it silently disables streamable-HTTP resumability), `mount_path=` (→ `http_app(path=...)`), `transport=` (→ `run(transport=...)`), `transport_security=` (→ host/origin settings on `http_app()`), `warn_on_duplicate_tools/_resources/_prompts=` (→ one `on_duplicate=`), `dependencies=` (→ a fastmcp.json file) +- `name`, `instructions`, `website_url`, `icons`, `tools`, `lifespan` carry over unchanged +- note when reporting: FastMCP names the streamable HTTP transport "http", not "streamable-http" + +CONTEXT METHODS WITH CHANGED SIGNATURES (compile fine, fail at runtime) +- `ctx.log(level, data)` → `ctx.log(message, level=...)`, message first +- `ctx.info(data)` / `debug` / `warning` / `error` → take a str message, not arbitrary JSON-serializable data +- `ctx.elicit(..., schema=Model)` → `response_type=Model` +- `ctx.read_resource(uri)` → returns a `ResourceResult`; read `.contents` rather than iterating the return value +- `ctx.report_progress`, `ctx.request_id`, `ctx.client_id` are unchanged + +AUTHENTICATION (the one case where the single import change is NOT enough) +- `token_verifier=` and `auth_server_provider=` — both raise TypeError on FastMCP 4 +- `auth=AuthSettings(...)` — the keyword survives but the value does not: FastMCP's `auth=` takes a FastMCP `AuthProvider`, not the SDK settings object +Report these as a real migration, not a rename: FastMCP consolidates all three into one provider, and ships `JWTVerifier` for tokens you already issue, `RemoteAuthProvider` for delegating to an external authorization server, `OAuthProxy` for wrapping a provider without Dynamic Client Registration, and named providers for GitHub, Google, Auth0, Keycloak, and others. Look up the right one at https://gofastmcp.com/servers/auth/authentication rather than guessing. + +PROMPT RETURN VALUES +- prompt functions returning `PromptMessage`, or `TextContent`-wrapped content +- prompt functions returning raw dicts with "role"/"content" keys — FastMCP 1.0 coerced these silently, standalone FastMCP does not + +OTHER mcp.* IMPORTS +- anything from `mcp.types` — the module does not exist in the SDK v2 that FastMCP 4 builds on; protocol types moved to `mcp_types` with camelCase fields renamed to snake_case +- `from mcp.server.stdio import stdio_server` and any transport boilerplate around it +- `mcp.types.TextContent` / `ImageContent` used to wrap tool return values — FastMCP has friendlier equivalents, so prefer those over a mechanical `mcp_types` swap + +DECORATOR RETURN VALUES +- any code reading `.name`, `.description`, or other component attributes off a `@mcp.tool` / `@mcp.resource` / `@mcp.prompt` decorated function. Decorators return the original function now. + +For each item found, show the original line, name what changed, and give the corrected code from the guide. If the only change needed is the import, say so plainly rather than manufacturing work. + + +## What Might Need Updating + +Most servers need nothing beyond the import change. Skim the sections below to see if any apply. + +### Constructor Settings + +If you passed transport settings like `host` or `port` directly to `FastMCP()`, those now belong on `run()`. This keeps your server definition independent of how it's deployed: + +```python test="skip" +from fastmcp import FastMCP + +# Before +mcp = FastMCP("my-server", host="0.0.0.0", port=8080) +mcp.run() + +# After +mcp = FastMCP("my-server") +mcp.run(transport="http", host="0.0.0.0", port=8080) +``` + +Nine arguments move this way, and each raises a `TypeError` naming its own replacement, so you can also just run the server and follow the errors: `host`, `port`, `log_level`, `debug`, `sse_path`, `message_path`, `streamable_http_path`, `json_response`, and `stateless_http`. + +A second group is rejected with only a generic "unexpected keyword argument" and no hint, which makes these the ones worth reading in advance: + +| SDK v1 `FastMCP(...)` | FastMCP 4 | +|---|---| +| `event_store=` | `mcp.http_app(event_store=...)` | +| `mount_path=` | `mcp.http_app(path=...)` | +| `transport=` | `mcp.run(transport=...)` | +| `transport_security=` | `host_origin_protection=`, `allowed_hosts=`, `allowed_origins=` on `http_app()` | +| `warn_on_duplicate_tools=`, `_resources=`, `_prompts=` | a single `on_duplicate=` | +| `dependencies=[...]` | a [`fastmcp.json`](/deployment/server-configuration) configuration file | +| `auth_server_provider=`, `token_verifier=` | a single `auth=` provider — see [Authentication](#authentication) below | + +Dropping `event_store=` rather than moving it is the one to watch: it silently disables streamable-HTTP resumability, so a client that reconnects loses the events it missed instead of replaying them. + +`name`, `instructions`, `website_url`, `icons`, `tools`, and `lifespan` carry over to the constructor unchanged. + +### Authentication + +This is the one case where the import change alone won't do. FastMCP 1.0 exposed the SDK's auth plumbing as three separate constructor arguments — `token_verifier=`, `auth_server_provider=`, and `auth=AuthSettings(...)`. The first two raise `TypeError` on FastMCP 4, and while `auth=` survives as a keyword, its value doesn't: FastMCP expects one of its own `AuthProvider` objects rather than the SDK's settings object. + +The replacement is a single provider carrying the whole configuration, chosen by what you're actually doing: + +| What you were doing | FastMCP provider | +|---|---| +| Validating JWTs you already issue | `JWTVerifier` | +| Delegating to an external authorization server | `RemoteAuthProvider` | +| Wrapping a provider without Dynamic Client Registration | `OAuthProxy` | +| GitHub, Google, Auth0, Keycloak, WorkOS, … | the matching named provider | + +```python +from fastmcp import FastMCP +from fastmcp.server.auth import JWTVerifier + +mcp = FastMCP("my-server", auth=JWTVerifier(jwks_uri="https://example.com/.well-known/jwks.json")) +``` + +See [Authentication](/servers/auth/authentication) for the full set and their configuration. + +### Context Methods + +`from fastmcp import Context` gets you the injected context object, but four of its methods took a different shape in FastMCP 1.0, and a bare import swap leaves calls that compile and then fail: + +| SDK v1 | FastMCP 4 | +|---|---| +| `ctx.log(level, data)` | `ctx.log(message, level=...)` — message is first now | +| `ctx.info(data)` and its `debug`/`warning`/`error` siblings | take a `str` message, where v1 accepted any JSON-serializable value | +| `ctx.elicit(message, schema=Model)` | `ctx.elicit(message, response_type=Model)` | +| `ctx.read_resource(uri)` | returns a `ResourceResult`; the payload is under `.contents` rather than being iterable directly | + +`ctx.report_progress()`, `ctx.request_id`, and `ctx.client_id` are unchanged. + +### Prompts + +If your prompt functions return `mcp.types.PromptMessage` objects or raw dicts with `role`/`content` keys, upgrade them to FastMCP's `Message` class. Or just return a plain string — it's automatically wrapped as a user message. FastMCP 1.0 silently coerced dicts into messages; standalone FastMCP requires typed `Message` objects or strings. + +```python +from fastmcp import FastMCP + +mcp = FastMCP("prompts") + +@mcp.prompt +def review(code: str) -> str: + """Review code for issues""" + return f"Please review this code:\n\n{code}" +``` + +Multi-turn prompts return a list of messages. `Message` takes the text positionally and defaults to the user role, so only the assistant turns need a `role`: + +```python +from fastmcp import FastMCP +from fastmcp.prompts import Message + +mcp = FastMCP("prompts") + +@mcp.prompt +def debug(error: str) -> list[Message]: + """Start a debugging session""" + return [ + Message(f"I'm seeing this error:\n\n{error}"), + Message("I'll help debug that. Can you share the relevant code?", role="assistant"), + ] +``` + +### Other `mcp.*` Imports + +FastMCP 4 builds on MCP SDK v2, where the `mcp.types` module no longer exists. Protocol types moved to a standalone `mcp_types` package, and the field names were renamed from camelCase to snake_case (`inputSchema` → `input_schema`, `mimeType` → `mime_type`, and so on). Update `from mcp.types import X` to `from mcp_types import X`. For everything else SDK v2 changed, see [Upgrading from FastMCP 3](/getting-started/upgrading/from-fastmcp-3), which covers the same protocol rebuild from the FastMCP side. + +Where FastMCP provides its own API for the same thing, it's worth switching over rather than importing the protocol type: + +| MCP SDK v1 | FastMCP equivalent | +|---|---| +| `mcp.types.TextContent(type="text", text=str(x))` | Just return `x` from your tool | +| `mcp.types.ImageContent(...)` | `from fastmcp.utilities.types import Image` | +| `mcp.types.PromptMessage(...)` | `from fastmcp.prompts import Message` | +| `mcp.server.fastmcp.Context` | `from fastmcp import Context` | +| `from mcp.server.stdio import stdio_server` | Not needed — `mcp.run()` handles transport | + +For protocol types without a FastMCP equivalent, import them from `mcp_types` directly. + +### Decorated Functions + +In FastMCP 1.0, `@mcp.tool` replaced your function with a `FunctionTool` object. Now decorators return your original function unchanged, so decorated functions stay callable for testing, reuse, and composition: + +```python +from fastmcp import FastMCP + +mcp = FastMCP("greeter") + +@mcp.tool +def greet(name: str) -> str: + """Greet someone""" + return f"Hello, {name}!" + +# This works now — the function is still a regular function +assert greet("World") == "Hello, World!" +``` + +Code that reads `.name`, `.description`, or other component attributes off the decorated result needs updating. This is uncommon — most servers never touch the tool object. When you do need the component itself, reach it through the server with `await mcp.get_tool("greet")`. + +## Verifying the Upgrade + +Run your server the way you always have. To confirm every component came across, inspect the server with the FastMCP CLI: + +```bash +fastmcp inspect my_server.py +``` + +The output lists every tool, resource, template, and prompt your server exposes, so a component that failed to register shows up here rather than at the first client call. + +## Looking Ahead + +The MCP ecosystem is evolving fast. Part of FastMCP's job is to absorb that complexity on your behalf — as the protocol and its tooling grow, we do the work so your server code doesn't have to change. The SDK v1 to v2 transition is the clearest example so far: an entire protocol layer was rewritten underneath FastMCP 4, and the servers on this page cross it with one line. diff --git a/docs/getting-started/upgrading/from-mcp-sdk-v2.mdx b/docs/getting-started/upgrading/from-mcp-sdk-v2.mdx new file mode 100644 index 000000000..6ca0578aa --- /dev/null +++ b/docs/getting-started/upgrading/from-mcp-sdk-v2.mdx @@ -0,0 +1,325 @@ +--- +title: Upgrading from MCP SDK v2 +sidebarTitle: "From MCP SDK v2" +description: Move a server built on the MCP Python SDK v2's MCPServer class to FastMCP +icon: up +--- + +If your server starts with `from mcp.server.mcpserver import MCPServer`, you're using the high-level server API introduced in v2 of the `mcp` package. Moving to FastMCP is a mechanical migration: the two APIs share a lineage, so most of your code carries over with a rename. + +```python +# Before +from mcp.server.mcpserver import MCPServer + +server = MCPServer("my-server") + +@server.tool() +def greet(name: str) -> str: + """Greet someone by name""" + return f"Hello, {name}!" + +# After +from fastmcp import FastMCP + +mcp = FastMCP("my-server") + +@mcp.tool +def greet(name: str) -> str: + """Greet someone by name""" + return f"Hello, {name}!" +``` + +That resemblance is not a coincidence. `MCPServer` is the SDK's successor to FastMCP 1.0, the high-level server that shipped inside SDK v1; FastMCP is the standalone framework that grew from the same starting point. Both derive the protocol layer from your function signatures — type hints become JSON Schema, docstrings become descriptions, return values are serialized for you. What separates them is scope: `MCPServer` is the SDK's ergonomic surface over the protocol, while FastMCP builds on that same SDK v2 and adds the machinery a server needs in production — composition, middleware, proxying, authentication providers, tool transformation, a client, and a testing story. + + +Building on the low-level `Server` class instead? See [Upgrading from the Low-Level SDK v2](/getting-started/upgrading/from-low-level-sdk-v2). Still on SDK v1's `mcp.server.fastmcp.FastMCP`? Your upgrade is a single import — see [Upgrading from MCP SDK v1](/getting-started/upgrading/from-mcp-sdk-v1). + + + +You are migrating an MCP server from the MCP Python SDK v2's high-level `MCPServer` class (`mcp.server.mcpserver`) to FastMCP 4. The two APIs are close relatives, so most of this is mechanical renaming. + +FIRST, fetch https://gofastmcp.com/getting-started/upgrading/from-mcp-sdk-v2 — it carries the full mapping table and before-and-after code for everything below. Fetch https://gofastmcp.com for anything the guide doesn't cover. Do not invent a FastMCP API you have not confirmed in the docs. + +Then work through the provided code looking for each of these. + +IMPORTS AND CONSTRUCTION +- `MCPServer`, and `Context`, `Image`, `Audio`, `Message` imported from `mcp.server.mcpserver` +- `mcp_types` imports — these are UNCHANGED. FastMCP 4 builds on the same SDK v2, so leave them alone and say so. + +DECORATORS +- `@server.tool()`, `@server.prompt()` — FastMCP takes a bare `@mcp.tool` / `@mcp.prompt` (and still accepts the called form) +- `@server.resource(...)`, `@server.completion()`, `@server.custom_route(...)` + +TRANSPORT +- `run(transport="streamable-http")` — FastMCP names this transport "http" +- `streamable_http_app()`, `sse_app()` + +CONSTRUCTOR ARGUMENTS THAT DO NOT CARRY OVER +- `debug=`, `log_level=` +- `warn_on_duplicate_tools=` / `_resources=` / `_prompts=` +- `dependencies=` +- `title=`, `description=` +- `token_verifier=`, `auth_server_provider=`, `auth=AuthSettings(...)` — FastMCP consolidates all three into one `auth=` provider +- `cache_hints=` +- `extensions=` +- `tools=[...]` (rare — the SDK's `Tool` type is not exported): FastMCP takes plain callables, so pass the underlying functions +These raise TypeError, most naming their replacement. `name`, `version`, `instructions`, `icons`, `website_url`, `lifespan`, `resource_security`, and `request_state_security` carry over unchanged. + +CONTEXT — these ten properties do NOT exist on FastMCP's Context and raise AttributeError if you only swap the import: +- `ctx.mcp_server` → `ctx.fastmcp` +- `ctx.headers` → `get_http_headers()` from `fastmcp.server.dependencies` (a function, not a property) +- `ctx.protocol_version` → `ctx.request_context.protocol_version` +- `ctx.client_capabilities` → read it off `ctx.session` / `ctx.request_context` +- `ctx.notify_tools_changed()`, `notify_resources_changed()`, `notify_prompts_changed()`, `notify_resource_updated()` → `ctx.send_notification(...)` with the matching `mcp_types` notification. FastMCP emits the list-changed ones for you when components change visibility through `ctx.enable_components` / `ctx.disable_components`. +- `ctx.elicit_url` → not the same thing as `ctx.elicit` (that one is form elicitation, with a different signature and wire behavior). The URL flow survives on the raw session as `ctx.session.elicit_url(...)` — use that rather than deleting an OAuth or payment handoff. +- `ctx.close_standalone_sse_stream` → no public FastMCP equivalent, and NOT on `ctx.request_context`. Flag it for the user. +These four exist on both but with DIFFERENT signatures, so a bare import swap compiles and then fails at runtime: +- `ctx.log(level, data)` → `ctx.log(message, level=...)` — the first positional argument is now the message, not the level +- `ctx.info(data)` / `debug` / `warning` / `error` → these take `message` as a string, where the SDK accepted any JSON-serializable `data` +- `ctx.elicit(message, schema=Model)` → `ctx.elicit(message, response_type=Model)` — the keyword was renamed +- `ctx.read_resource(uri)` → still takes a URI, but returns a `ResourceResult` whose payload is under `.contents`, where the SDK returned an iterable of content objects directly. Code that iterates or indexes the return value needs updating. + +Genuinely unchanged: `report_progress`, `request_id`, `client_id`, `input_responses`, `request_state`, `session`, and `request_context`. + +RESOLVERS — the one part that is not a rename, so check for it first +- any `Annotated[T, Resolve(fn)]` parameter, and the resolvers behind it +- resolvers returning `Elicit[...]`, `Sample`, or `ListRoots` +FastMCP has no resolver injection, but the underlying requests survive in a different shape: on a modern connection `Elicit`, `Sample`, and `ListRoots` all ride the guard pattern, where the tool returns an `InputRequiredResult` and the client answers on the next call. Do not tell the user these capabilities are simply unavailable. Flag every resolver with the guide's per-capability reasoning (server-side LLM call is usually better than guard-routed sampling; roots are often simplest as ordinary tool arguments) rather than picking a rewrite yourself. Also note that a resolved parameter is hidden from the tool's input schema, so replacing it with an ordinary argument changes the schema clients see. + +For each item found, show the original code, name what changed, and give the FastMCP equivalent from the guide. Call out anything you could not find a documented replacement for instead of inventing one. + + +## Install + +FastMCP 4 is in prerelease, so pin the exact version rather than installing unqualified — a bare `pip install fastmcp` or `uv add fastmcp` resolves to the latest *stable* release, which today is FastMCP 3: + +```bash +pip install "fastmcp==4.0.0b1" +# or +uv add "fastmcp==4.0.0b1" +``` + +An exact version pin installs even though it's a prerelease — neither installer needs `--pre` or `--prerelease allow` for a version this specific, only for an open-ended range. For a reproducible lockfile that also pins the prerelease protocol dependencies, see [Install the v4 Prerelease](/getting-started/upgrading/from-fastmcp-3#install-the-v4-prerelease). + +FastMCP 4 depends on the MCP SDK v2, so nothing you already import from `mcp_types` moves. That is the practical benefit of migrating at this version rather than an earlier one: you and FastMCP are on the same protocol layer, with the same snake_case field names and the same type package, so the migration touches only the server API. + +## The Mechanical Part + +Most of the work is renaming. This table covers the surfaces a typical `MCPServer` server touches: + +| MCP SDK v2 | FastMCP | +|---|---| +| `from mcp.server.mcpserver import MCPServer` | `from fastmcp import FastMCP` | +| `from mcp.server.mcpserver import Context` | `from fastmcp import Context` | +| `from mcp.server.mcpserver import Image, Audio` | `from fastmcp.utilities.types import Image, Audio` | +| `from mcp.server.mcpserver.prompts.base import Message` | `from fastmcp.prompts import Message` | +| `@server.tool()` | `@mcp.tool` | +| `@server.prompt()` | `@mcp.prompt` | +| `@server.resource("uri://x")` | `@mcp.resource("uri://x")` | +| `@server.completion()` | `@mcp.completion` | +| `@server.custom_route(path, methods)` | `@mcp.custom_route(path, methods)` | +| `server.run(transport="streamable-http")` | `mcp.run(transport="http")` | +| `server.streamable_http_app()` | `mcp.http_app()` | +| `server.sse_app()` | `mcp.http_app(transport="sse")` | +| `ctx.mcp_server` | `ctx.fastmcp` | +| `ctx.headers` | `get_http_headers()` from `fastmcp.server.dependencies` | +| `ctx.protocol_version` | `ctx.request_context.protocol_version` | +| `ctx.client_capabilities` | read it off `ctx.session` | +| `from mcp_types import X` | unchanged | + +Two of these are worth a sentence each. The decorators lose their parentheses: `MCPServer` required `@server.tool()` and raised a `TypeError` telling you so if you wrote `@server.tool`, while FastMCP accepts both forms, so `@mcp.tool` is the idiomatic spelling and `@mcp.tool()` keeps working if you'd rather not touch every line. And the streamable HTTP transport is named `"http"` in FastMCP rather than `"streamable-http"` — the transport is the same, and `mcp.run()` still defaults to stdio. + +Here is a complete server before and after. Nothing in the logic changes: + + + +```python Before +import json +from mcp.server.mcpserver import MCPServer, Context + +server = MCPServer("demo") + +@server.tool() +def greet(name: str) -> str: + """Greet someone by name""" + return f"Hello, {name}!" + +@server.tool() +async def process(items: list[str], ctx: Context) -> str: + """Process a batch of items""" + for i, item in enumerate(items): + await ctx.report_progress(i, len(items)) + return f"Processed {len(items)} items" + +@server.resource("config://app", mime_type="application/json") +def app_config() -> str: + """Application configuration""" + return json.dumps({"debug": False}) + +@server.resource("users://{user_id}/profile") +def profile(user_id: str) -> str: + """User profile by ID""" + return json.dumps({"id": user_id}) + +@server.prompt() +def summarize(text: str) -> str: + """Summarize text""" + return f"Summarize:\n\n{text}" + +if __name__ == "__main__": + server.run(transport="streamable-http") +``` + +```python After +import json +from fastmcp import FastMCP, Context + +mcp = FastMCP("demo") + +@mcp.tool +def greet(name: str) -> str: + """Greet someone by name""" + return f"Hello, {name}!" + +@mcp.tool +async def process(items: list[str], ctx: Context) -> str: + """Process a batch of items""" + for i, item in enumerate(items): + await ctx.report_progress(i, len(items)) + return f"Processed {len(items)} items" + +@mcp.resource("config://app", mime_type="application/json") +def app_config() -> str: + """Application configuration""" + return json.dumps({"debug": False}) + +@mcp.resource("users://{user_id}/profile") +def profile(user_id: str) -> str: + """User profile by ID""" + return json.dumps({"id": user_id}) + +@mcp.prompt +def summarize(text: str) -> str: + """Summarize text""" + return f"Summarize:\n\n{text}" + +if __name__ == "__main__": + mcp.run(transport="http") +``` + + + +## Constructor Arguments + +`FastMCP()` describes your server's identity and behavior; how it gets deployed is decided when you serve it. Several `MCPServer` constructor arguments move accordingly, and each raises a `TypeError` naming its replacement rather than being silently ignored. + +`name`, `version`, `instructions`, `icons`, `website_url`, `lifespan`, `resource_security`, and `request_state_security` all mean what they meant before. The rest map like this: + +| `MCPServer(...)` | FastMCP | +|---|---| +| `debug=True` | `FASTMCP_DEBUG` environment variable | +| `log_level="DEBUG"` | `run_http_async(log_level=...)` or `FASTMCP_LOG_LEVEL` | +| `warn_on_duplicate_tools`, `_resources`, `_prompts` | a single `on_duplicate=` | +| `dependencies=[...]` | a [`fastmcp.json`](/deployment/server-configuration) configuration file | +| `title=`, `description=` | `instructions=` | +| `tools=[Tool, ...]` | `tools=[callable, ...]`, or FastMCP's own `Tool` | +| `resources=[Resource, ...]` | no constructor keyword — register with `@mcp.resource` or `mcp.add_resource()` | +| `subscriptions=` | no equivalent — see below | +| `token_verifier=`, `auth_server_provider=`, `auth=AuthSettings(...)` | a single `auth=` provider | +| `cache_hints={...}` | `cache_ttl=`, `cache_scope=` | +| `extensions=[...]` | `mcp.add_extension(...)` | + +Authentication is the largest of these, and it consolidates rather than moves. `MCPServer` exposes the SDK's raw auth plumbing — a token verifier, an authorization-server provider, and an `AuthSettings` object, configured separately. FastMCP takes one `auth=` provider that carries the whole configuration, and ships providers for the common cases: `JWTVerifier` for validating tokens you already issue, `RemoteAuthProvider` for delegating to an external authorization server, `OAuthProxy` for wrapping a provider that lacks Dynamic Client Registration, and named providers for GitHub, Google, Auth0, Keycloak, WorkOS, and others. See [Authentication](/servers/auth/authentication). + +Two rows are worth reading before you delete the argument. `resources=` has no constructor equivalent, so pre-built `Resource` objects need registering through `@mcp.resource` or `mcp.add_resource()` instead — dropping the keyword silently drops the resources with it. And `subscriptions=`, which an `MCPServer` uses to plug in an external pub/sub bus so resource-update notifications reach clients across replicas, has no FastMCP equivalent at all. A multi-replica deployment that relies on it should confirm it can live without cross-replica subscription fan-out before migrating, because a mechanical rename removes that behavior without any error to warn you. + +### Serving HTTP + +Renaming `streamable_http_app()` to `http_app()` is only mechanical for a call with no arguments. The keywords were renamed and regrouped, so an existing call carries arguments `http_app()` does not accept: + +| SDK v2 | FastMCP | +|---|---| +| `streamable_http_app(streamable_http_path=...)` | `http_app(path=...)` | +| `sse_app(sse_path=...)` | `http_app(path=..., transport="sse")` | +| `sse_app(message_path=...)` | no equivalent | +| `transport_security=TransportSecuritySettings(...)` | `host_origin_protection=`, `allowed_hosts=`, `allowed_origins=` | +| `host=...` | pass to `mcp.run(host=...)` instead | + +`json_response`, `stateless_http`, `event_store`, and `retry_interval` keep their names. See [Deploying HTTP servers](/deployment/http) for the host and origin settings. + +### Stricter Arguments + +One behavior change survives the rename and is worth knowing before you migrate. `MCPServer` binds the arguments it recognizes and ignores the rest, so a call carrying an unexpected key succeeds. FastMCP declares `"additionalProperties": false` on every generated schema and enforces it, so the same call fails: + +```python test="skip" +# Against MCPServer: succeeds, "extra" ignored. +# Against FastMCP: raises, "extra" is not a parameter of greet(). +await client.call_tool("greet", {"name": "World", "extra": "surprise"}) +``` + +For most servers this is an improvement that costs nothing — a caller sending keys your tool never reads was already a bug. It matters if a client in your fleet passes extra metadata alongside real arguments, since those calls start failing the moment you migrate. Accept the extras explicitly as optional parameters if you need to keep them working. + +## Asking for Input + +This is the one part of the migration that is not a rename, so read it before you start if your tools use resolvers. + +`MCPServer` asks the client for things through dependency-injection resolvers. A tool parameter annotated `Annotated[T, Resolve(fn)]` is filled by running `fn` before the tool body, and the resolver can return a request marker — `Elicit[T]` to ask the user, `Sample` to borrow the client's model, `ListRoots` to fetch its roots — which the framework turns into the right wire interaction for whichever protocol era the connection negotiated: + +```python +from typing import Annotated +from pydantic import BaseModel +from mcp.server.mcpserver import MCPServer, Resolve, Elicit + +server = MCPServer("booking") + + +class Destination(BaseModel): + destination: str + + +def ask_destination() -> Elicit[Destination]: + return Elicit("Where would you like to fly?", Destination) + + +@server.tool() +def book_flight(dest: Annotated[Destination, Resolve(ask_destination)]) -> str: + """Book a flight""" + return f"Booked to {dest.destination}" +``` + +FastMCP has no equivalent annotation, and it makes the protocol era explicit instead of hiding it. Which replacement you want depends on which era your clients speak. + +On **handshake-era connections** (≤ 2025-11-25), a running tool asks the user directly with `ctx.elicit()`, and the call blocks until the answer arrives. Where the resolver returned a value or aborted the call, `ctx.elicit()` hands you the outcome to branch on, so declining and cancelling become cases your tool answers for itself: + +```python +from fastmcp import FastMCP, Context + +mcp = FastMCP("booking") + + +@mcp.tool +async def book_flight(ctx: Context) -> str: + """Book a flight""" + result = await ctx.elicit("Where would you like to fly?", response_type=str) + if result.action == "accept": + return f"Booked to {result.data}" + return "Booking cancelled" +``` + +On the **modern protocol** (2026-07-28), server-initiated requests are gone from the wire, so a tool asks by *returning* a description of what it needs. The client answers and calls the tool again with the answer attached, and the tool re-runs from the top. This is the [guard pattern](/servers/elicitation#elicitation-on-the-modern-protocol), and it reads the answers off `ctx.input_responses`. + +The two are era-gated in both directions: `ctx.elicit()` raises on a modern connection, and a guard result raises on a handshake one. A server that must serve both branches on `ctx.request_context.protocol_version`. See [Elicitation](/servers/elicitation#which-approach-to-use) for both shapes side by side. + +Resolvers that return `Sample` or `ListRoots` have no *injected* equivalent — FastMCP has no `ctx.sample()` or `ctx.list_roots()` — but the underlying request survives, so this is a change of shape rather than a loss of capability. On a modern connection both ride the same guard pattern as elicitation: the tool returns an `InputRequiredResult` describing the sampling or roots request, and the client answers on the next call. + +Which shape you want differs by capability. For **roots**, the guard route is the natural replacement, since one round buys the whole answer — and taking the paths as ordinary tool arguments is simpler still whenever the caller can supply them. For **generation**, prefer [calling an LLM from your server](/servers/sampling) with your own API key: your tool then behaves identically for every client, including the many that never implemented sampling, and you avoid paying a full request-response cycle per generation step. Reach for the guard route when using the *caller's* model is specifically the point. + +One schema detail is easy to miss during the rewrite. A resolved parameter never appears in the tool's input schema — `book_flight` above advertises no arguments at all. When you replace a resolver with an explicit tool argument, the schema the client sees gains a field, which is usually what you want but is a visible change to your tool's contract. + +## What You Gain + +The migration is worth doing for what sits on the other side of it. FastMCP is a framework rather than a protocol surface, and these are the capabilities that most often motivate the move: + +[Server composition](/servers/composition) mounts one server inside another, so a large surface splits into modules that are developed and tested independently. [Middleware](/servers/middleware) runs across every request for logging, rate limiting, error handling, and caching, with hooks at whichever level of specificity you need. [Proxy servers](/servers/providers/proxy) put a FastMCP server in front of any existing MCP server, bridging transports and adding auth to a backend you don't control. The [OpenAPI integration](/integrations/openapi) generates a whole server from an existing API specification. [Tool transformation](/servers/transforms/transforms) rewrites the tools a server exposes — renaming, hiding, and reshaping arguments — without touching the code that defines them. + +FastMCP also ships a [client](/clients/client), which `MCPServer` has no counterpart for. It speaks every transport, drives both protocol eras, and connects to a server object in-process — so [testing](/servers/testing) a server means calling its tools in the same Python process, with no subprocess and no network. diff --git a/docs/getting-started/upgrading/from-mcp-sdk.mdx b/docs/getting-started/upgrading/from-mcp-sdk.mdx deleted file mode 100644 index 1954ceb72..000000000 --- a/docs/getting-started/upgrading/from-mcp-sdk.mdx +++ /dev/null @@ -1,166 +0,0 @@ ---- -title: Upgrading from the MCP SDK -sidebarTitle: "From MCP SDK" -description: Upgrade from FastMCP in the MCP Python SDK to the standalone FastMCP framework -icon: up ---- - -If your server starts with `from mcp.server.fastmcp import FastMCP`, you're using FastMCP 1.0 — the version bundled with v1 of the `mcp` package. Upgrading to the standalone FastMCP framework is easy. **For most servers, it's a single import change.** - -```python -# Before -from mcp.server.fastmcp import FastMCP - -# After -from fastmcp import FastMCP -``` - -That's it. Your `@mcp.tool`, `@mcp.resource`, and `@mcp.prompt` decorators, your `mcp.run()` call, and the rest of your server code all work as-is. - - -**Why upgrade?** FastMCP 1.0 pioneered the Pythonic MCP server experience, and we're proud it was bundled into the `mcp` package. The standalone FastMCP project has since grown into a full framework for taking MCP servers from prototype to production — with composition, middleware, proxy servers, authentication, and much more. Upgrading gives you access to all of that, plus ongoing updates and fixes. - - -## Install - -```bash -pip install --upgrade fastmcp -# or -uv add fastmcp -``` - -FastMCP includes the `mcp` package as a dependency, so you don't lose access to anything. Update your import, run your server, and if your tools work, you're done. - - -You are upgrading an MCP server from FastMCP 1.0 (bundled in the `mcp` package v1) to standalone FastMCP 4. Analyze the provided code and identify every change needed. The full upgrade guide is at https://gofastmcp.com/getting-started/upgrading/from-mcp-sdk and the complete FastMCP documentation is at https://gofastmcp.com — fetch these for complete context. - -STEP 1 — IMPORT (required for all servers): -Change "from mcp.server.fastmcp import FastMCP" to "from fastmcp import FastMCP". - -STEP 2 — CONSTRUCTOR KWARGS (only if FastMCP() receives transport settings): -FastMCP() no longer accepts: host, port, log_level, debug, sse_path, streamable_http_path, json_response, stateless_http. -Fix: pass these to run() instead. -Before: `mcp = FastMCP("server", host="0.0.0.0", port=8080); mcp.run()` -After: `mcp = FastMCP("server"); mcp.run(transport="http", host="0.0.0.0", port=8080)` - -STEP 3 — PROMPTS (only if using PromptMessage directly or returning dicts): -mcp.types.PromptMessage is replaced by fastmcp.prompts.Message. -Before: `PromptMessage(role="user", content=TextContent(type="text", text="Hello"))` -After: `Message("Hello")` — role defaults to "user", accepts plain strings. -Also: if prompts return raw dicts like `{"role": "user", "content": "..."}`, these must become Message objects or plain strings. -The MCP SDK's FastMCP 1.0 silently coerced dicts; standalone FastMCP requires typed returns. - -STEP 4 — OTHER MCP IMPORTS (only if importing from mcp.* directly): -FastMCP now builds on MCP SDK v2, which removed the `mcp.types` module — protocol types live in the standalone `mcp_types` package. Update any `from mcp.types import X` to `from mcp_types import X`. Prefer FastMCP's own APIs where equivalents exist: -- mcp_types.TextContent for tool returns → just return plain Python values (str, int, dict, etc.) -- mcp_types.ImageContent → fastmcp.utilities.types.Image -- from mcp.server.stdio import stdio_server → not needed, mcp.run() handles transport - -STEP 5 — DECORATORS (only if treating decorated functions as objects): -@mcp.tool, @mcp.resource, @mcp.prompt now return the original function, not a component object. Code that accesses .name or .description on the decorated result needs updating. Set FASTMCP_DECORATOR_MODE=object temporarily to restore v1 behavior (this compat setting is itself deprecated). - -For each issue found, show the original line, explain what changed, and provide the corrected code. - - -## What Might Need Updating - -Most servers need nothing beyond the import change. Skim the sections below to see if any apply. - -### Constructor Settings - -If you passed transport settings like `host` or `port` directly to `FastMCP()`, those now belong on `run()`. This keeps your server definition independent of how it's deployed: - -```python -# Before -mcp = FastMCP("my-server", host="0.0.0.0", port=8080) -mcp.run() - -# After -mcp = FastMCP("my-server") -mcp.run(transport="http", host="0.0.0.0", port=8080) -``` - -If you pass the old kwargs, you'll get a clear `TypeError` with a migration hint. - -### Prompts - -If your prompt functions return `mcp.types.PromptMessage` objects or raw dicts with `role`/`content` keys, you'll need to upgrade to FastMCP's `Message` class. Or just return a plain string — it's automatically wrapped as a user message. The MCP SDK's bundled FastMCP 1.0 silently coerced dicts into messages; standalone FastMCP requires typed `Message` objects or strings. - -```python -from fastmcp import FastMCP - -mcp = FastMCP("prompts") - -@mcp.prompt -def review(code: str) -> str: - """Review code for issues""" - return f"Please review this code:\n\n{code}" -``` - -For multi-turn prompts: - -```python -from fastmcp.prompts import Message - -@mcp.prompt -def debug(error: str) -> list[Message]: - """Start a debugging session""" - return [ - Message(f"I'm seeing this error:\n\n{error}"), - Message("I'll help debug that. Can you share the relevant code?", role="assistant"), - ] -``` - -### Other `mcp.*` Imports - -FastMCP now builds on MCP SDK v2. The `mcp.types` module no longer exists — protocol types moved to a standalone `mcp_types` package, and the field names were renamed from camelCase to snake_case (`inputSchema` → `input_schema`, `mimeType` → `mime_type`, and so on). Update `from mcp.types import X` to `from mcp_types import X`. For the full picture, see [Upgrading from FastMCP 3](/getting-started/upgrading/from-fastmcp-3). - -Where FastMCP provides its own API for the same thing, it's worth switching over: - -| mcp Package | FastMCP Equivalent | -|---|---| -| `mcp.types.TextContent(type="text", text=str(x))` | Just return `x` from your tool | -| `mcp.types.ImageContent(...)` | `from fastmcp.utilities.types import Image` | -| `mcp.types.PromptMessage(...)` | `from fastmcp.prompts import Message` | -| `from mcp.server.stdio import stdio_server` | Not needed — `mcp.run()` handles transport | - -For protocol types without a FastMCP equivalent, import them from `mcp_types` directly. - -### Decorated Functions - -In FastMCP 1.0, `@mcp.tool` returned a `FunctionTool` object. Now decorators return your original function unchanged — so decorated functions stay callable for testing, reuse, and composition: - -```python -@mcp.tool -def greet(name: str) -> str: - """Greet someone""" - return f"Hello, {name}!" - -# This works now — the function is still a regular function -assert greet("World") == "Hello, World!" -``` - -If you have code that accesses `.name`, `.description`, or other attributes on the decorated result, that will need updating. This is uncommon — most servers don't interact with the tool object directly. If you need the old behavior temporarily, set `FASTMCP_DECORATOR_MODE=object` to restore it (this compatibility setting is itself deprecated and will be removed in a future release). - -## Verify the Upgrade - -```bash -# Install -pip install --upgrade fastmcp - -# Check version -fastmcp version - -# Run your server -python my_server.py -``` - -You can also inspect your server's registered components with the FastMCP CLI: - -```bash -fastmcp inspect my_server.py -``` - -## Looking Ahead - -The MCP ecosystem is evolving fast. Part of FastMCP's job is to absorb that complexity on your behalf — as the protocol and its tooling grow, we do the work so your server code doesn't have to change. diff --git a/docs/servers/elicitation.mdx b/docs/servers/elicitation.mdx index 3798d6e7c..b69c2ecf1 100644 --- a/docs/servers/elicitation.mdx +++ b/docs/servers/elicitation.mdx @@ -27,7 +27,7 @@ Elicitation reaches the user two different ways, depending on the protocol era t - **On handshake-era connections (≤ 2025-11-25)**, a running tool calls [`ctx.elicit()`](#requesting-input-on-handshake-connections). The tool pauses mid-execution, the server sends a request over the session back-channel, and the tool resumes with the answer. This is the original elicitation API and the rest of this page's first half covers it in full. - **On the modern protocol (2026-07-28)**, that back-channel is gone — server-initiated requests were removed from the wire (SEP-2577), so a tool cannot issue a request mid-execution and block on the answer. Instead a tool asks for input by *returning* a description of what it needs; each round completes normally and the client issues a new call with the answer attached. This is the [guard pattern](#elicitation-on-the-modern-protocol), covered in the second half. -The era gate is strict: `ctx.elicit()` only works on handshake connections, and the guard pattern only works on modern ones. A tool that returns a guard result on a handshake connection — or calls `ctx.elicit()` on a modern one — raises a clear era error rather than failing obscurely. A server that serves both eras may need both paths; branch on `ctx.protocol_version` to pick the right one. `fastmcp.Client` drives whichever the connection negotiated automatically. +The era gate is strict: `ctx.elicit()` only works on handshake connections, and the guard pattern only works on modern ones. A tool that returns a guard result on a handshake connection — or calls `ctx.elicit()` on a modern one — raises a clear era error rather than failing obscurely. A server that serves both eras may need both paths; branch on `ctx.request_context.protocol_version` to pick the right one. `fastmcp.Client` drives whichever the connection negotiated automatically. ## Requesting input on handshake connections @@ -539,7 +539,7 @@ connection negotiated '2025-11-25'. Use ctx.elicit() for server-initiated input on handshake-era connections. ``` -If you need to support both eras, branch on `ctx.protocol_version`: return an `InputRequiredResult` on modern connections and fall back to [`ctx.elicit()`](#requesting-input-on-handshake-connections) on handshake-era ones. +If you need to support both eras, branch on `ctx.request_context.protocol_version`: return an `InputRequiredResult` on modern connections and fall back to [`ctx.elicit()`](#requesting-input-on-handshake-connections) on handshake-era ones. ### Prompts and resources diff --git a/fastmcp_slim/README.md b/fastmcp_slim/README.md index 9afc7f4f7..4a813d96b 100644 --- a/fastmcp_slim/README.md +++ b/fastmcp_slim/README.md @@ -100,9 +100,10 @@ uv pip install fastmcp For full installation instructions, including verification and upgrading, see the [**Installation Guide**](https://gofastmcp.com/getting-started/installation). **Upgrading?** We have guides for: -- [Upgrading from FastMCP v2](https://gofastmcp.com/getting-started/upgrading/from-fastmcp-2) -- [Upgrading from the MCP Python SDK](https://gofastmcp.com/getting-started/upgrading/from-mcp-sdk) -- [Upgrading from the low-level SDK](https://gofastmcp.com/getting-started/upgrading/from-low-level-sdk) +- [Upgrading from FastMCP 3](https://gofastmcp.com/getting-started/upgrading/from-fastmcp-3) +- [Upgrading from FastMCP 2](https://gofastmcp.com/getting-started/upgrading/from-fastmcp-2) +- [Upgrading from MCP SDK v1](https://gofastmcp.com/getting-started/upgrading/from-mcp-sdk-v1) or [v2](https://gofastmcp.com/getting-started/upgrading/from-mcp-sdk-v2) +- [Upgrading from the low-level SDK v1](https://gofastmcp.com/getting-started/upgrading/from-low-level-sdk-v1) or [v2](https://gofastmcp.com/getting-started/upgrading/from-low-level-sdk-v2) ## 📚 Documentation diff --git a/tests/docs/test_upgrade_guide_api_claims.py b/tests/docs/test_upgrade_guide_api_claims.py new file mode 100644 index 000000000..c580a4380 --- /dev/null +++ b/tests/docs/test_upgrade_guide_api_claims.py @@ -0,0 +1,250 @@ +"""Check the API claims the upgrade guides make against the real APIs. + +The other two doc tests cover code blocks: one executes them, one compares the +before/after pair. Neither looks at *prose*, and prose is where a migration +guide does most of its work — mapping tables, prompt checklists, and sentences +naming an attribute to use. Those claims went wrong repeatedly and in the same +way: an API was named without anyone checking it resolved. + +So this file checks the claims mechanically: + +- every ``ctx.`` the guides tell a reader to *use* exists on the class + they'd be using it on, and every one they name as removed really is gone +- every ``MCPServer`` constructor parameter appears somewhere in the SDK v2 + guide, so a newly added SDK argument can't quietly go unmapped +- the ``request_context`` attributes the guides route people to are real + +Run: + uv run pytest tests/docs/test_upgrade_guide_api_claims.py -v +""" + +from __future__ import annotations + +import inspect +import re +import warnings +from pathlib import Path +from typing import Any + +import pytest + +UPGRADE_DIR = Path("docs/getting-started/upgrading") + + +def _guide(name: str) -> str: + return (UPGRADE_DIR / name).read_text("utf-8") + + +with warnings.catch_warnings(): + warnings.simplefilter("ignore") + from mcp.server.mcpserver import MCPServer + + from fastmcp import Context as FastMCPContext + + +# Context attributes the guides may mention without them existing on FastMCP's +# Context, because the guide's whole point is that they are gone or moved. Each +# is asserted to genuinely be absent, so a name that later gains an +# implementation stops being listed as missing. +DOCUMENTED_AS_ABSENT = { + "sample", + "sample_step", + "list_roots", + "mcp_server", + "headers", + "protocol_version", + "client_capabilities", + "elicit_url", + "close_standalone_sse_stream", + "notify_tools_changed", + "notify_resources_changed", + "notify_prompts_changed", + "notify_resource_updated", + "params", + "meta", +} + + +def test_absent_context_attributes_are_really_absent(): + """Names the guides describe as gone must not exist on FastMCP's Context. + + If one of these gains an implementation, the guides are now telling people + to work around something that works, and this test says so. + """ + resurrected = [ + n for n in sorted(DOCUMENTED_AS_ABSENT) if hasattr(FastMCPContext, n) + ] + assert not resurrected, ( + f"guides describe these as absent from fastmcp.Context, but they exist: {resurrected}" + ) + + +@pytest.mark.parametrize( + "guide", + sorted(p.name for p in UPGRADE_DIR.glob("*.mdx")), +) +def test_ctx_attributes_named_in_guides_exist(guide: str): + """Every ``ctx.`` in a guide either exists or is documented as absent.""" + referenced = set(re.findall(r"`ctx\.([a-z_]+)", _guide(guide))) + unknown = { + name + for name in referenced + if not hasattr(FastMCPContext, name) and name not in DOCUMENTED_AS_ABSENT + } + assert not unknown, ( + f"{guide} names ctx.{{{', '.join(sorted(unknown))}}}, which do not exist on " + f"fastmcp.Context and are not in DOCUMENTED_AS_ABSENT" + ) + + +def test_request_context_attributes_the_guides_route_to_exist(): + """The guides send people to ``ctx.request_context`` for several attributes. + + ``FastMCPRequestContext`` resolves its attributes dynamically, so this is + checked against a live request rather than the class. + """ + import asyncio + + from fastmcp import Client, FastMCP + + mcp = FastMCP("probe") + + @mcp.tool + async def probe(ctx: FastMCPContext) -> list[str]: + rc = ctx.request_context + return [n for n in ("request_id", "meta", "protocol_version") if hasattr(rc, n)] + + async def run() -> list[str]: + async with Client(mcp) as client: + return (await client.call_tool("probe", {})).data + + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + present = asyncio.run(run()) + + assert set(present) == {"request_id", "meta", "protocol_version"} + + +# Context methods the SDK v2 guide says are *genuinely* unchanged. Existence is +# not enough for that claim — a method present on both classes with a different +# signature is worse than a missing one, because the import swap compiles and +# fails at runtime. So these are compared signature-for-signature. +CLAIMED_SIGNATURE_COMPATIBLE = ["report_progress"] + +# Present on both, but with signatures that differ. The guide must describe each +# migration rather than list it as carrying over; this pins the difference so a +# future SDK or FastMCP release that converges them shows up as a failure. +KNOWN_SIGNATURE_DIFFERENCES = ["log", "info", "debug", "warning", "error", "elicit"] + + +@pytest.mark.parametrize("method", CLAIMED_SIGNATURE_COMPATIBLE) +def test_methods_claimed_unchanged_have_identical_signatures(method: str): + from mcp.server.mcpserver import Context as SDKContext + + sdk = inspect.signature(getattr(SDKContext, method)) + fastmcp = inspect.signature(getattr(FastMCPContext, method)) + assert str(sdk) == str(fastmcp), ( + f"the SDK v2 guide lists ctx.{method} as carrying over unchanged, but " + f"the signatures differ:\n SDK : {sdk}\n FastMCP: {fastmcp}" + ) + + +@pytest.mark.parametrize("method", KNOWN_SIGNATURE_DIFFERENCES) +def test_methods_with_known_signature_differences_still_differ(method: str): + from mcp.server.mcpserver import Context as SDKContext + + sdk = inspect.signature(getattr(SDKContext, method)) + fastmcp = inspect.signature(getattr(FastMCPContext, method)) + assert str(sdk) != str(fastmcp), ( + f"ctx.{method} signatures now match; the guide's migration note for it " + f"is stale and should be moved to the unchanged list" + ) + + +# SDK v1's `mcp.server.fastmcp.FastMCP.__init__` parameters. Hardcoded because +# v1 cannot be installed alongside v4 to introspect — read from the published +# mcp 1.20.0 wheel. Anything here that FastMCP 4 does not accept must appear in +# the v1 guide, since a reader following "it's one import change" hits it. +SDK_V1_CONSTRUCTOR_PARAMS = [ + "name", "instructions", "website_url", "icons", "auth_server_provider", + "token_verifier", "event_store", "tools", "debug", "log_level", "host", + "port", "mount_path", "sse_path", "message_path", "streamable_http_path", + "json_response", "stateless_http", "warn_on_duplicate_resources", + "warn_on_duplicate_tools", "warn_on_duplicate_prompts", "dependencies", + "lifespan", "auth", "transport_security", "transport", +] # fmt: skip + + +def test_sdk_v1_constructor_params_fastmcp_rejects_are_documented(): + """Every v1 keyword FastMCP 4 refuses must be named in the v1 guide. + + The guide's headline is that upgrading is a single import change. That is + only honest if the constructor arguments it *doesn't* accept are spelled + out, so nobody follows the headline into a ``TypeError``. + """ + from fastmcp import FastMCP + + guide = _guide("from-mcp-sdk-v1.mdx") + probe: dict[str, Any] = { + "name": "s", + "icons": None, + "tools": None, + "lifespan": None, + } + + undocumented = [] + for param in SDK_V1_CONSTRUCTOR_PARAMS: + if param == "name": + continue + kwargs: dict[str, Any] = {param: probe.get(param)} + try: + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + FastMCP("s", **kwargs) + continue # accepted, nothing to document + except TypeError: + pass + except Exception: + continue # accepted the keyword, rejected the probe value + shorthand = param.replace("warn_on_duplicate", "") + if re.search(rf"`{re.escape(param)}[=`]", guide): + continue + if param.startswith("warn_on_duplicate") and re.search( + rf"`{re.escape(shorthand)}[=`]", guide + ): + continue + undocumented.append(param) + + assert not undocumented, ( + "SDK v1 FastMCP() parameters that FastMCP 4 rejects but from-mcp-sdk-v1.mdx " + f"never mentions: {undocumented}" + ) + + +def test_every_mcpserver_constructor_param_is_mapped(): + """The SDK v2 guide claims an exhaustive constructor mapping — hold it to that. + + A parameter added to ``MCPServer`` upstream should fail here rather than + reach a reader as an unmapped keyword that raises ``TypeError`` on FastMCP. + """ + guide = _guide("from-mcp-sdk-v2.mdx") + params = [ + p for p in inspect.signature(MCPServer.__init__).parameters if p != "self" + ] + + unmapped = [] + for param in params: + # `warn_on_duplicate_resources` is covered by the table's shorthand + # "warn_on_duplicate_tools, _resources, _prompts". + shorthand = param.replace("warn_on_duplicate", "") + if re.search(rf"`{re.escape(param)}[=`]", guide): + continue + if param.startswith("warn_on_duplicate") and re.search( + rf"`{re.escape(shorthand)}`", guide + ): + continue + unmapped.append(param) + + assert not unmapped, ( + f"MCPServer constructor parameters not mentioned in from-mcp-sdk-v2.mdx: {unmapped}" + ) diff --git a/tests/docs/test_upgrade_guide_equivalence.py b/tests/docs/test_upgrade_guide_equivalence.py new file mode 100644 index 000000000..0549ff228 --- /dev/null +++ b/tests/docs/test_upgrade_guide_equivalence.py @@ -0,0 +1,239 @@ +"""Prove the SDK v2 upgrade guides produce an equivalent server. + +`test_upgrade_guide_examples.py` proves every example runs. That is necessary +but not sufficient: a migration guide is only correct if the "after" code +exposes the same MCP surface as the "before" code it replaces. A guide whose +halves both run but disagree on a tool's schema teaches a silent regression. + +So for each MCP SDK v2 guide, the complete before-and-after server pair is +lifted out of the page, both halves are built, and their advertised tools, +resources, templates, and prompts are compared. The SDK v1 guides are not +covered here — v1 is not installable alongside v4, so their "before" code +cannot be built to compare against. + +Run: + uv run pytest tests/docs/test_upgrade_guide_equivalence.py -v +""" + +from __future__ import annotations + +from pathlib import Path +from typing import Any +from uuid import uuid4 + +import pytest +from pytest_examples.find_examples import _extract_code_chunks + +from fastmcp import Client, FastMCP + +UPGRADE_DIR = Path("docs/getting-started/upgrading") + + +def _block_containing(page: str, needle: str) -> dict[str, Any]: + """Execute the one code block on `page` that contains `needle`.""" + path = UPGRADE_DIR / page + matches = [ + ex + for ex in _extract_code_chunks(path, path.read_text("utf-8"), uuid4()) + if needle in ex.source + ] + assert len(matches) == 1, ( + f"expected exactly one block in {page} containing {needle!r}, " + f"found {len(matches)}" + ) + namespace: dict[str, Any] = {"__name__": "fastmcp_docs_example"} + exec(compile(matches[0].source, str(path), "exec"), namespace) + return namespace + + +def _strip_titles(node: Any) -> Any: + """Recursively drop every "title" key, the one difference that's genuinely cosmetic. + + A hand-written SDK schema has no title anywhere; FastMCP derives one at every + level from the function/model it built the schema from. Everything else in the + tree — constraints, "additionalProperties", nested "anyOf"/"const", enum values — + is retained, because those describe what a client is allowed to send and a + silent difference there is exactly the kind of regression this test exists to + catch. + """ + if isinstance(node, dict): + return {k: _strip_titles(v) for k, v in node.items() if k != "title"} + if isinstance(node, list): + return [_strip_titles(v) for v in node] + return node + + +def _normalize(schema: dict[str, Any] | None) -> dict[str, Any]: + """Compare schemas by their full structure, modulo generated titles. + + "required" is sorted because the SDK and FastMCP may build it in a different + parameter order for the same signature — an ordering difference, not a + contract difference. + """ + if not schema: + return {} + stripped = _strip_titles(schema) + if "required" in stripped: + stripped["required"] = sorted(stripped["required"]) + return stripped + + +def _split_declared_strictness( + before: dict[str, dict[str, Any]], after: dict[str, dict[str, Any]] +) -> dict[str, dict[str, Any]]: + """Pop `"additionalProperties": false` from every migrated schema, asserting it's there. + + FastMCP's generated tool schemas declare `"additionalProperties": false`; a + schema built by either SDK server API does not declare it. This is a real + contract change, not a cosmetic one — both SDK APIs *accept* an unexpected + argument at call time, and FastMCP rejects it (pinned by + `test_fastmcp_tightens_the_argument_contract` in each class below). It is + popped here only so the rest of the schema can be compared field for field, + and popping is an assertion rather than a silent discard: if FastMCP ever + stops declaring it, or the SDK starts, this fails. + """ + stripped: dict[str, dict[str, Any]] = {} + for name, schema in after.items(): + schema = dict(schema) + assert schema.pop("additionalProperties", None) is False, ( + f"expected FastMCP to declare additionalProperties: false for {name!r}" + ) + assert "additionalProperties" not in before.get(name, {}), ( + f"expected the SDK schema for {name!r} not to declare additionalProperties" + ) + stripped[name] = schema + return stripped + + +async def _fastmcp_surface(mcp: FastMCP) -> dict[str, Any]: + async with Client(mcp) as client: + tools = await client.list_tools() + resources = await client.list_resources() + templates = await client.list_resource_templates() + prompts = await client.list_prompts() + return { + "tools": {t.name: _normalize(t.input_schema) for t in tools}, + "resources": {str(r.uri) for r in resources}, + "templates": {t.uri_template for t in templates}, + "prompts": {p.name: sorted(a.name for a in p.arguments or []) for p in prompts}, + } + + +class TestMCPServerGuide: + """docs/.../from-mcp-sdk-v2.mdx — the high-level MCPServer migration.""" + + @pytest.fixture(scope="class") + def pair(self) -> tuple[Any, FastMCP]: + before = _block_containing("from-mcp-sdk-v2.mdx", 'MCPServer("demo")') + after = _block_containing("from-mcp-sdk-v2.mdx", 'FastMCP("demo")') + return before["server"], after["mcp"] + + async def test_same_surface(self, pair): + server, mcp = pair + + before = { + "tools": { + t.name: _normalize(t.input_schema) for t in await server.list_tools() + }, + "resources": {str(r.uri) for r in await server.list_resources()}, + "templates": { + t.uri_template for t in await server.list_resource_templates() + }, + "prompts": { + p.name: sorted(a.name for a in p.arguments or []) + for p in await server.list_prompts() + }, + } + + after = await _fastmcp_surface(mcp) + after["tools"] = _split_declared_strictness(before["tools"], after["tools"]) + assert before == after + + async def test_fastmcp_tightens_the_argument_contract(self, pair): + """FastMCP rejects an unexpected argument where MCPServer accepts it. + + This is the behavior behind the `additionalProperties` schema difference, + and it is a real change for any caller that was passing extra keys. + """ + server, mcp = pair + + tolerated = await server.call_tool( + "greet", {"name": "World", "extra": "surprise"} + ) + assert tolerated.is_error is False + assert tolerated.content[0].text == "Hello, World!" + + async with Client(mcp) as client: + with pytest.raises(Exception): + await client.call_tool("greet", {"name": "World", "extra": "surprise"}) + + async def test_migrated_tools_still_work(self, pair): + _, mcp = pair + async with Client(mcp) as client: + greeting = await client.call_tool("greet", {"name": "World"}) + processed = await client.call_tool("process", {"items": ["a", "b"]}) + + assert greeting.data == "Hello, World!" + assert processed.data == "Processed 2 items" + + +class TestLowLevelGuide: + """docs/.../from-low-level-sdk-v2.mdx — the low-level Server migration.""" + + @pytest.fixture(scope="class") + def pair(self) -> tuple[dict[str, Any], FastMCP]: + before = _block_containing("from-low-level-sdk-v2.mdx", ' "demo",') + after = _block_containing("from-low-level-sdk-v2.mdx", 'FastMCP("demo")') + return before, after["mcp"] + + async def test_same_surface(self, pair): + handlers, mcp = pair + + tools = await handlers["list_tools"](None, None) + resources = await handlers["list_resources"](None, None) + prompts = await handlers["list_prompts"](None, None) + before = { + "tools": {t.name: _normalize(t.input_schema) for t in tools.tools}, + "resources": {str(r.uri) for r in resources.resources}, + "templates": set(), + "prompts": { + p.name: sorted(a.name for a in p.arguments or []) + for p in prompts.prompts + }, + } + + after = await _fastmcp_surface(mcp) + after["tools"] = _split_declared_strictness(before["tools"], after["tools"]) + assert before == after + + async def test_fastmcp_tightens_the_argument_contract(self, pair): + """FastMCP rejects an unexpected argument where the handler ignored it. + + A low-level handler reads `params.arguments` as a plain dict and never + looks at keys it doesn't need, so extras pass through silently. The + migrated tool rejects them. Pinned rather than normalized away, because + it is a real change for any caller that was passing extra keys. + """ + handlers, mcp = pair + params = type( + "Params", (), {"name": "greet", "arguments": {"name": "World", "extra": 1}} + )() + + tolerated = await handlers["call_tool"](None, params) + assert tolerated.content[0].text == "Hello, World!" + + async with Client(mcp) as client: + with pytest.raises(Exception): + await client.call_tool("greet", {"name": "World", "extra": 1}) + + async def test_handlers_and_tools_agree(self, pair): + """The rewritten tool returns what the hand-written handler returned.""" + handlers, mcp = pair + params = type("Params", (), {"name": "greet", "arguments": {"name": "World"}})() + + handler_result = await handlers["call_tool"](None, params) + async with Client(mcp) as client: + tool_result = await client.call_tool("greet", {"name": "World"}) + + assert handler_result.content[0].text == "Hello, World!" + assert tool_result.data == "Hello, World!" diff --git a/tests/docs/test_upgrade_guide_examples.py b/tests/docs/test_upgrade_guide_examples.py new file mode 100644 index 000000000..142c662b4 --- /dev/null +++ b/tests/docs/test_upgrade_guide_examples.py @@ -0,0 +1,101 @@ +"""Execute the Python examples in the upgrade guides. + +`test_doc_examples.py` covers every page in `docs/`, but only checks that +examples parse and that their ``fastmcp.*`` imports resolve. The upgrade guides +carry a stronger obligation: someone lands on one mid-migration, copies a block, +and runs it. So these examples are actually executed, and their non-FastMCP +imports (`mcp`, `mcp_types`) are exercised along with everything else. + +Both halves of a `` are executed where they can be. The "after" code +is FastMCP 4, which this repo is. The "before" code is only runnable when it +targets the MCP SDK **v2** — the version installed here — which covers the two +SDK v2 guides. Blocks written against SDK v1 (whose `mcp.types` and +`mcp.server.fastmcp` no longer exist) and fragments that pair a "# Before" and +"# After" in one block are tagged ``test="skip"`` in the source and skipped here; +the count of those is pinned so a new one can't appear unnoticed. + +Run: + uv run pytest tests/docs/test_upgrade_guide_examples.py -v +""" + +from __future__ import annotations + +import warnings +from pathlib import Path +from uuid import uuid4 + +import pytest +from pytest_examples import CodeExample +from pytest_examples.find_examples import _extract_code_chunks + +import fastmcp + +UPGRADE_DIR = Path("docs/getting-started/upgrading") + +# Blocks deliberately not executable: SDK v1 API that is no longer installable, +# and before/after fragments that are not standalone programs. Pinned so that +# adding a skip is a visible decision rather than a silent one. +EXPECTED_SKIPS = 35 + + +def _examples() -> list[CodeExample]: + examples: list[CodeExample] = [] + for mdx_file in sorted(UPGRADE_DIR.rglob("*.mdx")): + code = mdx_file.read_text("utf-8") + examples.extend(_extract_code_chunks(mdx_file, code, uuid4())) + return examples + + +ALL = _examples() +RUNNABLE = [ex for ex in ALL if ex.prefix_settings().get("test") != "skip"] +SKIPPED = [ex for ex in ALL if ex.prefix_settings().get("test") == "skip"] + + +def _example_id(example: CodeExample) -> str: + return f"{Path(example.path).name}:{example.start_line}" + + +def test_guides_have_examples(): + """Guard against the extractor silently matching nothing.""" + assert len(RUNNABLE) >= 20, f"only found {len(RUNNABLE)} runnable examples" + + +def test_skip_count_is_pinned(): + """A newly unrunnable example should be a deliberate choice.""" + listing = "\n".join(f" {_example_id(ex)}" for ex in SKIPPED) + assert len(SKIPPED) == EXPECTED_SKIPS, ( + f"expected {EXPECTED_SKIPS} skipped examples, found {len(SKIPPED)}:\n{listing}" + ) + + +@pytest.fixture(autouse=True) +def restore_global_settings(): + """Undo any global setting an example changes. + + Some examples exist precisely to show a global toggle — the upgrade guide + demonstrates turning the camelCase bridge off with + ``fastmcp.settings.mcp_camelcase_compat = False``. Executing that here + would otherwise leave the bridge off for every test that runs afterwards in + the same process, which silently breaks unrelated suites. + """ + before = fastmcp.settings.model_dump() + yield + for field, value in before.items(): + if getattr(fastmcp.settings, field, value) != value: + setattr(fastmcp.settings, field, value) + + +@pytest.mark.parametrize("example", RUNNABLE, ids=[_example_id(e) for e in RUNNABLE]) +def test_example_executes(example: CodeExample): + """Every non-skipped example runs top to bottom without raising. + + Examples are executed under a module name other than ``__main__`` so an + ``if __name__ == "__main__": mcp.run()`` footer defines the server without + starting it. + """ + namespace: dict[str, object] = {"__name__": "fastmcp_docs_example"} + with warnings.catch_warnings(): + # Guides intentionally demonstrate deprecated surfaces (the camelCase + # bridge, SDK logging) whose warnings are the point being made. + warnings.simplefilter("ignore") + exec(compile(example.source, str(example.path), "exec"), namespace) From a8b5da9770ce5ba0cd7710d469889409852184f6 Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Tue, 28 Jul 2026 10:54:19 -0400 Subject: [PATCH 12/53] Late-bind app tool names so UIs survive composition (#4682) --- docs/apps/architecture.mdx | 38 +- docs/apps/fastmcp-app.mdx | 6 +- docs/apps/low-level.mdx | 4 +- fastmcp_slim/fastmcp/apps/app.py | 41 +- .../fastmcp/server/providers/addressing.py | 12 +- .../fastmcp/server/providers/aggregate.py | 32 +- fastmcp_slim/fastmcp/server/providers/base.py | 6 +- .../server/providers/prefab_payload.py | 158 +++++ .../server/providers/prefab_synthesis.py | 5 +- .../fastmcp/server/providers/proxy.py | 50 +- fastmcp_slim/fastmcp/server/server.py | 172 +++-- fastmcp_slim/fastmcp/tools/base.py | 14 +- fastmcp_slim/fastmcp/tools/tool_transform.py | 46 +- tests/apps/test_file_upload.py | 10 +- .../server/providers/test_prefab_roundtrip.py | 603 +++++++++++++++++- tests/test_fastmcp_app.py | 109 +++- tests/tools/tool_transform/test_metadata.py | 39 ++ 17 files changed, 1182 insertions(+), 163 deletions(-) create mode 100644 fastmcp_slim/fastmcp/server/providers/prefab_payload.py diff --git a/docs/apps/architecture.mdx b/docs/apps/architecture.mdx index 4eaad9023..727697094 100644 --- a/docs/apps/architecture.mdx +++ b/docs/apps/architecture.mdx @@ -61,17 +61,43 @@ The final tool result has two parts: `content` (a list of `TextContent` blocks f ## Tool call routing -Normal tool calls go through the provider chain, which applies transforms (namespace prefixes, visibility filters) before resolving by name. App UI calls need a different path. +A tool has two things that behave very differently. Its **name** is unstable by design — namespace transforms rename it, so `save_contact` becomes `contacts_save_contact` in one composition and something else in another. Its **identity** is a hash of the app name and the registered tool name, written once at registration and never changed. -### The hashed lookup bypass +A UI is serialized during the entry tool's call, deep inside whatever composition the server happens to have, so it cannot know what its backend tools will be called by the time the payload reaches a host. -Backend tools are typically hidden from the model (`visibility=["app"]`). Visibility transforms would filter them out of normal resolution. And namespace transforms might rename them — `save_contact` becomes `contacts_save_contact` — while the renderer needs a stable way to call the original backend. +### Late-bound tool names -Hashed lookup solves both problems. FastMCP first tries normal tool resolution. If no visible tool matches and the requested name looks like `_`, FastMCP calls `get_tool_by_hash(hash, local_name)`. That lookup walks the provider tree directly, skipping transforms. It finds an app-visible tool by its original registered name and verifies that its stored `meta["fastmcp"]["_tool_hash"]` matches the requested hash. +The payload leaves the app addressed by identity, and every FastMCP server rewrites those references on the way out to whatever it lists that tool as. Servers unwind innermost-first, so the outermost server rewrites last — and its names are the only ones a client can actually invoke. -That's why `CallTool(save_contact)` keeps working when the server is mounted under a namespace. The renderer sends a deterministic hashed backend name; the server uses `get_tool_by_hash` to find the original tool without transforms in the way. +Rewriting a name in place would destroy the identity for the next layer up, so the payload carries a name-to-identity map under `_meta.fastmcp.toolNames`. Each layer resolves through the map and updates it. The action objects keep the exact shape `prefab_ui` defines: only the value of `tool` changes, and only ever to another valid tool name. -Authorization still applies. The hashed bypass skips name and visibility transforms, but auth checks still run against the tool's `auth` config before execution. +The result is that a renderer receives names that exist in the listing the host is looking at. Under three layers of namespacing the button calls `c_b_a_save`; behind a gateway it calls whatever the gateway lists. No intermediary has to understand a FastMCP-specific convention. + +A reference this server cannot resolve is left alone rather than corrupted. This is what keeps apps working behind [tool search](/servers/transforms/tool-search) and code mode, which replace `tools/list` with a handful of synthetic tools: there is no better name to bind to, so the reference stays identity-addressed and the fallback below carries it. + +### One copy of an app per server + +**An app name must be unique within a server.** Composing the same app twice breaks its UI, and no namespace or mount arrangement makes it work. + +The reason is structural. Identity is derived from the app name and the tool's registered name, and deliberately nothing else — that is what makes it survive renaming. Two copies of one app therefore produce two tools claiming a single identity, and no fact anywhere in the listing says which copy a given button belongs to. The information needed to choose was never recorded. + +FastMCP declines to bind rather than picking a copy, so buttons stop working instead of quietly invoking the wrong tenant's tool. Expect a message naming the cause: + +``` +Ambiguous app tool 'save': 2 components share the identity '10c0803009ff'. +The same app is composed more than once, so this call cannot be routed to a +single tool. +``` + +Give each copy its own app name. Two tenants running the same product want `FastMCPApp("contacts-acme")` and `FastMCPApp("contacts-globex")` — not two instances of `FastMCPApp("contacts")` under different namespaces, since namespaces rename tools and identity is immune to renaming by design. + +### The hashed lookup fallback + +The identity-addressed form `_` remains callable. FastMCP first tries normal tool resolution; if no tool matches and the name has that shape, it calls `get_tool_by_hash(hash, local_name)`, which walks the provider tree directly, skipping transforms. + +When one identity is claimed by more than one tool — which happens when the same app is composed into two branches — the call is refused rather than resolved, since picking either one would silently route into the wrong branch. + +Authorization still applies. The hashed path skips name and visibility transforms, but auth checks still run against the tool's `auth` config before execution. ### Provider delegation diff --git a/docs/apps/fastmcp-app.mdx b/docs/apps/fastmcp-app.mdx index 55b3b7ed7..b3facd212 100644 --- a/docs/apps/fastmcp-app.mdx +++ b/docs/apps/fastmcp-app.mdx @@ -89,7 +89,11 @@ A fair question. Any [Interactive Tool](/apps/prefab) can call a server tool — - What happens to `CallTool("add_note")` when you mount this server under a namespace and the tool becomes `notes_add_note`? - How do you keep it all wired correctly as you compose servers? -`FastMCPApp` owns these concerns. Entry points register as model-visible. Backend tools register as UI-only by default. Backend tools get globally stable identifiers that survive namespacing, and `CallTool` accepts function references, so references stay valid when you compose servers. +`FastMCPApp` owns these concerns. Entry points register as model-visible, backend tools register as UI-only, and hosts act on those declarations to decide what the model sees. + +Composition is handled by never writing the name down. `CallTool` takes a function reference, and FastMCP resolves it when the UI is serialized — to whatever that tool is actually called by then. Mount the server under a namespace and the button calls `notes_add_note`; put a gateway in front and it calls whatever the gateway lists. Since you never wrote a name, renaming cannot break it. [The architecture page](/apps/architecture) covers how that resolution works. + +The one rule that comes with this: **an app name must be unique within a server.** Composing the same app twice breaks its UI — two copies of `FastMCPApp("notes")` are indistinguishable no matter what namespaces you mount them under, so FastMCP declines to bind rather than picking one. Name apps for what they serve: `FastMCPApp("notes-acme")` and `FastMCPApp("notes-globex")`. [The architecture page](/apps/architecture) explains why identity works this way. The rest of this page covers each piece in turn. diff --git a/docs/apps/low-level.mdx b/docs/apps/low-level.mdx index 14f5d331d..fc07e5b8d 100644 --- a/docs/apps/low-level.mdx +++ b/docs/apps/low-level.mdx @@ -70,11 +70,13 @@ def my_tool() -> str: The `visibility` field controls where a tool appears: - `["model"]` — visible to the LLM (the default behavior) -- `["app"]` — only callable from within the app UI, hidden from the LLM +- `["app"]` — callable from within the app UI, kept out of the LLM's tool list - `["model", "app"]` — both This is useful when you have tools that only make sense as part of the app's interactive flow, not as standalone LLM actions. +Visibility is a declaration, not server-side filtering. Every tool appears in `tools/list` carrying its `visibility` metadata, and the host decides what to show the model — the division the MCP Apps specification defines. Listing them is also what lets a proxy or gateway forward them: an intermediary can only route to a tool it can see. + ```python @mcp.tool( app=AppConfig( diff --git a/fastmcp_slim/fastmcp/apps/app.py b/fastmcp_slim/fastmcp/apps/app.py index 33e606118..8a4dc64d0 100644 --- a/fastmcp_slim/fastmcp/apps/app.py +++ b/fastmcp_slim/fastmcp/apps/app.py @@ -54,16 +54,20 @@ F = TypeVar("F", bound=Callable[..., Any]) def _make_resolver(app_name: str | None = None) -> Any: - """Create a CallTool resolver that prefixes tool names with a hash. + """Create a CallTool resolver that addresses peer tools by identity. - Structurally identical to the old ``___`` resolver — ``app_name`` is - the FastMCPApp's name, known at serialization time from the tool's - ``meta["fastmcp"]["app"]`` tag. The only change is the wire format: - ``_`` instead of ``___``. + ``app_name`` is the FastMCPApp's name, known at serialization time from + the tool's ``meta["fastmcp"]["app"]`` tag. Serialization happens deep + inside whatever composition the server has, so nothing here can know + what these tools will be *called* by the time the payload reaches a + host. References therefore start out identity-addressed, as + ``_``. - The dispatcher recognizes the hashed form and routes it via - ``get_tool_by_hash`` which walks the provider tree recursively — - same pattern as ``get_app_tool``. + Each FastMCP server rewrites those references on the way out to the + name it lists that tool under, so what a renderer finally receives is + an ordinary tool name (see ``server.providers.prefab_payload``). A + reference no server could resolve keeps this form, which the dispatcher + still routes via ``get_tool_by_hash``. """ from fastmcp.server.providers.addressing import ( hashed_backend_name, @@ -227,14 +231,17 @@ class FastMCPApp(Provider): raise ValueError(f"Cannot determine tool name for {fn!r}") from fastmcp.apps.config import AppConfig, app_config_to_meta_dict - from fastmcp.server.providers.addressing import hash_tool + from fastmcp.server.providers.addressing import ( + TOOL_HASH_META_KEY, + hash_tool, + ) app_config = AppConfig(visibility=visibility) meta: dict[str, Any] = { "ui": app_config_to_meta_dict(app_config), "fastmcp": { "app": self.name, - "_tool_hash": hash_tool(self.name, resolved_name), + TOOL_HASH_META_KEY: hash_tool(self.name, resolved_name), }, } @@ -318,7 +325,10 @@ class FastMCPApp(Provider): def _register(fn: F, tool_name: str | None) -> F: from fastmcp.apps.config import AppConfig, app_config_to_meta_dict - from fastmcp.server.providers.addressing import hash_tool + from fastmcp.server.providers.addressing import ( + TOOL_HASH_META_KEY, + hash_tool, + ) from fastmcp.server.providers.local_provider.decorators.tools import ( PREFAB_RENDERER_URI, ) @@ -334,7 +344,7 @@ class FastMCPApp(Provider): "ui": app_config_to_meta_dict(app_config), "fastmcp": { "app": self.name, - "_tool_hash": hash_tool(self.name, resolved), + TOOL_HASH_META_KEY: hash_tool(self.name, resolved), }, } @@ -373,12 +383,15 @@ class FastMCPApp(Provider): if not isinstance(tool, Tool): tool = Tool._ensure_tool(tool) - from fastmcp.server.providers.addressing import hash_tool + from fastmcp.server.providers.addressing import ( + TOOL_HASH_META_KEY, + hash_tool, + ) meta = dict(tool.meta) if tool.meta else {} fm = meta.setdefault("fastmcp", {}) fm["app"] = self.name - fm["_tool_hash"] = hash_tool(self.name, tool.name) + fm[TOOL_HASH_META_KEY] = hash_tool(self.name, tool.name) ui = meta.setdefault("ui", {}) if "visibility" not in ui: ui["visibility"] = ["app"] diff --git a/fastmcp_slim/fastmcp/server/providers/addressing.py b/fastmcp_slim/fastmcp/server/providers/addressing.py index 71a70a077..7a9b09d4b 100644 --- a/fastmcp_slim/fastmcp/server/providers/addressing.py +++ b/fastmcp_slim/fastmcp/server/providers/addressing.py @@ -13,9 +13,16 @@ app name + tool name. The hash serves two purposes: and ``read_resource`` synthesize these on demand from the tool's meta. The hash is computed at registration time from ``(app_name, tool_name)`` — -both known at that moment — and stored in ``meta["fastmcp"]["_tool_hash"]``. +both known at that moment — and stored in ``meta["fastmcp"]["tool_hash"]``. Deterministic across replicas (same code → same hash), no registry walk needed. + +The key is deliberately public. Keys prefixed with ``_`` inside the +``fastmcp`` meta namespace are stripped at every serialization boundary +(see ``FastMCPComponent.get_meta``) because they hold process-local state +such as enabled/disabled marks. The hash is the opposite: a stable +identity that intermediaries need in order to recognize a tool they are +forwarding, so it must survive the wire. """ from __future__ import annotations @@ -25,6 +32,9 @@ import hashlib #: Length of the hex hash prefix used in URIs and backend-tool names. HASH_LENGTH = 12 +#: Key inside the ``fastmcp`` meta namespace holding a tool's identity hash. +TOOL_HASH_META_KEY = "tool_hash" + def hash_tool(app_name: str, tool_name: str) -> str: """Deterministic hex hash for a tool in an app. diff --git a/fastmcp_slim/fastmcp/server/providers/aggregate.py b/fastmcp_slim/fastmcp/server/providers/aggregate.py index 53928d055..b6ee36ae9 100644 --- a/fastmcp_slim/fastmcp/server/providers/aggregate.py +++ b/fastmcp_slim/fastmcp/server/providers/aggregate.py @@ -25,7 +25,7 @@ from collections.abc import AsyncIterator, Sequence from contextlib import AsyncExitStack, asynccontextmanager from typing import TYPE_CHECKING, Literal, TypeVar -from fastmcp.exceptions import NotFoundError +from fastmcp.exceptions import NotFoundError, ToolError from fastmcp.server.providers.base import Provider from fastmcp.server.transforms import Namespace from fastmcp.utilities.async_utils import gather @@ -221,19 +221,41 @@ class AggregateProvider(Provider): return None async def get_tool_by_hash(self, tool_hash: str, tool_name: str) -> Tool | None: - """Query all child providers for a tool matching a hash.""" + """Query all child providers for a tool matching a hash. + + The hash identifies a tool by app name and registered name, with no + mount-point component, so composing one app into two branches yields + two distinct tools claiming the same identity. That is ambiguous + rather than resolvable: picking either one silently routes a UI's + call into the wrong branch. Raise instead. + + An ambiguity raised by a child is a verdict, not a provider failure, + so it propagates whatever the error strategy is. Swallowing it would + turn a duplicated app into "unknown tool", which sends whoever hits + it looking for a missing registration instead of a duplicate one. + """ results = await gather( (p.get_tool_by_hash(tool_hash, tool_name) for p in self.providers), return_exceptions=True, ) + matches: list[Tool] = [] for r in results: if isinstance(r, BaseException): - if self.provider_error_strategy == "raise": + if isinstance(r, ToolError) or self.provider_error_strategy == "raise": raise r continue if r is not None: - return r - return None + matches.append(r) + + if not matches: + return None + if len(matches) > 1: + raise ToolError( + f"Ambiguous app tool {tool_name!r}: {len(matches)} components share " + f"the identity {tool_hash!r}. The same app is composed more than " + f"once, so this call cannot be routed to a single tool." + ) + return matches[0] # ------------------------------------------------------------------------- # Resources diff --git a/fastmcp_slim/fastmcp/server/providers/base.py b/fastmcp_slim/fastmcp/server/providers/base.py index 959dc9de9..7941039dd 100644 --- a/fastmcp_slim/fastmcp/server/providers/base.py +++ b/fastmcp_slim/fastmcp/server/providers/base.py @@ -214,9 +214,11 @@ class Provider: """Look up an app-visible tool by its deterministic hash. Same recursive-walk semantics as ``get_app_tool`` but matches on - ``meta["fastmcp"]["_tool_hash"]`` instead of the app name tag. + ``meta["fastmcp"]["tool_hash"]`` instead of the app name tag. Used by the dispatcher when receiving hashed backend-tool calls. """ + from fastmcp.server.providers.addressing import TOOL_HASH_META_KEY + tool = await self._get_tool(tool_name) if tool is not None: meta = tool.meta or {} @@ -227,7 +229,7 @@ class Provider: ) if ( isinstance(fastmcp_meta, dict) - and fastmcp_meta.get("_tool_hash") == tool_hash + and fastmcp_meta.get(TOOL_HASH_META_KEY) == tool_hash and "app" in visibility ): return tool diff --git a/fastmcp_slim/fastmcp/server/providers/prefab_payload.py b/fastmcp_slim/fastmcp/server/providers/prefab_payload.py new file mode 100644 index 000000000..9741954d1 --- /dev/null +++ b/fastmcp_slim/fastmcp/server/providers/prefab_payload.py @@ -0,0 +1,158 @@ +"""Late-bound tool names in Prefab UI payloads. + +A Prefab UI is serialized during the entry tool's call, deep inside whatever +composition the server happens to have. At that moment nothing knows what the +backend tools will be *called* by the time the payload reaches a host: every +layer above may rename them, and the outermost layer's names are the only ones +a client can actually invoke. + +So the payload leaves the app addressed by identity — ``_``, +stable everywhere — and every FastMCP server rewrites those references on the +way out to whatever it lists that tool as. Servers rewrite innermost-first, so +the edge writes last and wins. + +Rewriting a name in place would destroy the identity for the next layer up, so +the payload carries a name-to-identity map under ``_meta.fastmcp.toolNames``. +Each layer resolves through the map and updates it. The action objects keep the +exact shape ``prefab_ui`` defines — only the value of ``tool`` changes, and only +ever to another valid tool name. + +Renderers read ``_meta`` already and ignore keys they don't recognize, so this +needs no renderer change. +""" + +from __future__ import annotations + +from collections.abc import Callable +from typing import Any + +from fastmcp.server.providers.addressing import parse_hashed_backend_name + +#: Action discriminator emitted by ``prefab_ui``'s ``CallTool``. +_TOOL_CALL_ACTION = "toolCall" + +_META_KEY = "_meta" +_FASTMCP_KEY = "fastmcp" +_TOOL_NAMES_KEY = "toolNames" + +#: Resolves an identity hash to the name this server lists that tool under. +#: Returns None when the identity cannot be resolved here, in which case the +#: existing reference is left alone. +IdentityResolver = Callable[[str], str | None] + + +def _walk_tool_calls(node: Any) -> list[dict[str, Any]]: + """Collect every ``toolCall`` action object in a payload tree.""" + found: list[dict[str, Any]] = [] + if isinstance(node, dict): + if node.get("action") == _TOOL_CALL_ACTION and isinstance( + node.get("tool"), str + ): + found.append(node) + for value in node.values(): + found.extend(_walk_tool_calls(value)) + elif isinstance(node, list): + for item in node: + found.extend(_walk_tool_calls(item)) + return found + + +def _read_map(payload: dict[str, Any]) -> dict[str, str]: + meta = payload.get(_META_KEY) + if not isinstance(meta, dict): + return {} + fastmcp_meta = meta.get(_FASTMCP_KEY) + if not isinstance(fastmcp_meta, dict): + return {} + names = fastmcp_meta.get(_TOOL_NAMES_KEY) + if not isinstance(names, dict): + return {} + return {k: v for k, v in names.items() if isinstance(k, str) and isinstance(v, str)} + + +def _write_map(payload: dict[str, Any], names: dict[str, str]) -> None: + meta = payload.setdefault(_META_KEY, {}) + if not isinstance(meta, dict): + return + fastmcp_meta = meta.setdefault(_FASTMCP_KEY, {}) + if not isinstance(fastmcp_meta, dict): + return + fastmcp_meta[_TOOL_NAMES_KEY] = names + + +def payload_has_identities(payload: Any) -> bool: + """Cheap guard: does this payload carry tool references worth rewriting? + + Runs on every tool result, so it must not walk the tree. + """ + return isinstance(payload, dict) and bool(_read_map(payload)) + + +def annotate_payload_identities(payload: dict[str, Any]) -> dict[str, Any]: + """Record the identity-addressed form of each reference, at serialization. + + References start out as ``_``, so the map begins as an + identity map to itself. Once a later layer rewrites a name, this is the + only remaining route back: it carries both what the reference points at + and the address any server can fall back to. + """ + if not isinstance(payload, dict): + return payload + + addresses: dict[str, str] = dict(_read_map(payload)) + for action in _walk_tool_calls(payload): + tool_name = action["tool"] + if tool_name in addresses: + continue + if parse_hashed_backend_name(tool_name) is not None: + addresses[tool_name] = tool_name + + if addresses: + _write_map(payload, addresses) + return payload + + +def rewrite_payload_tool_names( + payload: Any, + resolve: IdentityResolver, +) -> Any: + """Re-address a payload's tool references to this server's own names. + + Mutates in place and returns the payload. + + A reference this server cannot resolve is restored to its + identity-addressed form rather than left as-is. Leaving it would strand + whatever name an inner server chose — a name that is correct there and + meaningless here — and, unlike the identity form, a stranded name has no + route back. Restoring keeps the reference resolvable by the dispatcher, + or by any server further out with a better view. + """ + if not isinstance(payload, dict): + return payload + + addresses = _read_map(payload) + if not addresses: + return payload + + rebound: dict[str, str] = {} + for current_name, address in addresses.items(): + parsed = parse_hashed_backend_name(address) + new_name = resolve(parsed[0]) if parsed is not None else None + if new_name is None: + new_name = address + if new_name != current_name: + rebound[current_name] = new_name + + if not rebound: + return payload + + for action in _walk_tool_calls(payload): + new_name = rebound.get(action["tool"]) + if new_name is not None: + action["tool"] = new_name + + _write_map( + payload, + {rebound.get(name, name): address for name, address in addresses.items()}, + ) + return payload diff --git a/fastmcp_slim/fastmcp/server/providers/prefab_synthesis.py b/fastmcp_slim/fastmcp/server/providers/prefab_synthesis.py index e47bd0391..111cd677e 100644 --- a/fastmcp_slim/fastmcp/server/providers/prefab_synthesis.py +++ b/fastmcp_slim/fastmcp/server/providers/prefab_synthesis.py @@ -2,7 +2,7 @@ Tools marked as Prefab (via ``app=True``, ``PrefabAppConfig``, etc.) carry a placeholder ``meta.ui.resourceUri`` and optionally a hash in -``meta.fastmcp._tool_hash``. This module synthesizes per-tool renderer +``meta.fastmcp.tool_hash``. This module synthesizes per-tool renderer resources on demand at ``list_resources`` and ``read_resource`` time without storing or materializing anything. @@ -19,6 +19,7 @@ from typing import TYPE_CHECKING, Any, cast from fastmcp.server.providers.addressing import ( HASH_LENGTH, + TOOL_HASH_META_KEY, hash_tool, parse_hashed_resource_uri, ) @@ -48,7 +49,7 @@ def _get_tool_hash(tool: Tool) -> str | None: meta = tool.meta or {} fastmcp_meta = meta.get("fastmcp") if isinstance(fastmcp_meta, dict): - h = fastmcp_meta.get("_tool_hash") + h = fastmcp_meta.get(TOOL_HASH_META_KEY) if isinstance(h, str) and len(h) == HASH_LENGTH: return h # Fall back to computing from app name diff --git a/fastmcp_slim/fastmcp/server/providers/proxy.py b/fastmcp_slim/fastmcp/server/providers/proxy.py index 55d51f420..0d7ef3a78 100644 --- a/fastmcp_slim/fastmcp/server/providers/proxy.py +++ b/fastmcp_slim/fastmcp/server/providers/proxy.py @@ -39,7 +39,7 @@ from fastmcp.client.sampling import create_sampling_callback from fastmcp.client.telemetry import client_span from fastmcp.client.transports import ClientTransportT from fastmcp.client.transports.base import TransportOptions -from fastmcp.exceptions import ResourceError +from fastmcp.exceptions import ResourceError, ToolError from fastmcp.mcp_config import MCPConfig from fastmcp.prompts import Message, Prompt, PromptResult from fastmcp.prompts.base import InputRequiredPromptResult, PromptArgument @@ -856,6 +856,54 @@ class ProxyProvider(Provider): return None return max(matching, key=version_sort_key) + async def get_tool_by_hash(self, tool_hash: str, tool_name: str) -> Tool | None: + """Resolve an identity against the remote listing. + + The base implementation looks the tool up by its registered name, + which assumes the name survived to here. Across a proxy it need not: + a backend that mounts its app under a namespace advertises + ``crm_save``, and nothing named ``save`` was ever listed. Matching on + the identity carried in meta is what the identity is for. + + A remote that mounts one app twice sends back two tools claiming one + identity, exactly as a local composition would. That is refused here + on the same terms ``AggregateProvider`` refuses it, so a duplicated + app is caught wherever it is composed rather than only nearby. + """ + from fastmcp.server.providers.addressing import TOOL_HASH_META_KEY + + cache = self._tools_cache + if cache is None or not cache.is_fresh(self._cache_ttl): + await self._list_tools() + cache = self._tools_cache + assert cache is not None + + matches: list[Tool] = [] + for tool in cache.items: + meta = tool.meta or {} + fastmcp_meta = meta.get("fastmcp") + ui_meta = meta.get("ui") + visibility = ( + ui_meta.get("visibility", []) if isinstance(ui_meta, dict) else [] + ) + if ( + isinstance(fastmcp_meta, dict) + and fastmcp_meta.get(TOOL_HASH_META_KEY) == tool_hash + and "app" in visibility + ): + matches.append(tool) + + if not matches: + return None + distinct = {tool.name for tool in matches} + if len(distinct) > 1: + raise ToolError( + f"Ambiguous app tool {tool_name!r}: {len(distinct)} components share " + f"the identity {tool_hash!r}. The same app is composed more than " + f"once, so this call cannot be routed to a single tool." + ) + return max(matches, key=version_sort_key) + # ------------------------------------------------------------------------- # Resource methods # ------------------------------------------------------------------------- diff --git a/fastmcp_slim/fastmcp/server/server.py b/fastmcp_slim/fastmcp/server/server.py index 14d199958..6fb97bace 100644 --- a/fastmcp_slim/fastmcp/server/server.py +++ b/fastmcp_slim/fastmcp/server/server.py @@ -145,8 +145,9 @@ def _version_request_meta( # The MCP SDK warns "Tool X not listed, no validation will be performed" -# for every call to app-only tools (hidden from list_tools by design). -# This fires even when validate_input=False. Suppress it. +# for every call addressed by hashed backend name, since that address is +# an identity rather than a listed tool name. This fires even when +# validate_input=False. Suppress it. class _SuppressUnlistedToolWarning(logging.Filter): def filter(self, record: logging.LogRecord) -> bool: return "not listed, no validation" not in record.getMessage() @@ -223,64 +224,18 @@ def _get_auth_context() -> tuple[bool, Any]: return (False, get_access_token()) -def _is_backend_tool(tool: Tool) -> bool: - """Check whether a tool is handled specially as backend tool +def _tool_identity(tool: Tool) -> str | None: + """Read a tool's stable identity hash, if it carries one.""" + from fastmcp.server.providers.addressing import TOOL_HASH_META_KEY - Tools registered via ``@app.tool()`` (without ``model=True``) have - ``meta["ui"]["visibility"] == ["app"]`` — they are callable by app UIs - but should not appear in tool list the client passes to the model. - - They are handled specially for in various ways - e.g. they are looked - up via get_app_tool(), and don't appear in the tools/list output. - (FIXME: the latter isn't correct behavior according to the mcp-apps spec.) - - Returns True (a backend tool) when: - - The tool has ``meta.fastmcp.app``. - - The tool has ``meta.ui.visibility``. - - The visibility is precisely ``["app"]``. - - Returns False otherwise. - """ meta = tool.meta if not meta: - return False - fastmcp = meta.get("fastmcp") - if not isinstance(fastmcp, dict): - return False - if fastmcp.get("app") is None: - return False - ui = meta.get("ui") - if not isinstance(ui, dict): - return False - visibility = ui.get("visibility") - if not isinstance(visibility, list): - return False - return len(visibility) == 1 and visibility[0] == "app" - - -def _is_app_visible(tool: Tool) -> bool: - """Check whether a tool has explicitly opted into app-callable visibility. - - Gates the dispatcher's hashed-name routing path: only tools whose - ``meta.ui.visibility`` list contains ``"app"`` can be reached via - ``_`` calls. Tools without an explicit visibility - declaration are NOT app-callable — they must be reached by their - display name through the normal transform-aware resolution path. - - This is the inverse of the "everything is dot-callable" trap: the - hashed-name path is an opt-in mechanism for FastMCPApp backend tools, - not a general bypass for arbitrary tools. - """ - meta = tool.meta - if not meta: - return False - ui = meta.get("ui") - if not isinstance(ui, dict): - return False - visibility = ui.get("visibility") - if not isinstance(visibility, list): - return False - return "app" in visibility + return None + fastmcp_meta = meta.get("fastmcp") + if not isinstance(fastmcp_meta, dict): + return None + identity = fastmcp_meta.get(TOOL_HASH_META_KEY) + return identity if isinstance(identity, str) else None @asynccontextmanager @@ -728,7 +683,7 @@ class FastMCP( """Replace placeholder Prefab URIs with per-tool hashed ones. For each tool whose ``meta.ui.resourceUri`` is the placeholder, - reads the tool's stored hash from ``meta.fastmcp._tool_hash`` + reads the tool's stored hash from ``meta.fastmcp.tool_hash`` and rewrites the URI to the per-tool form. Also strips CSP from tool meta (it belongs on the resource). Produces ``model_copy`` views — originals are untouched. @@ -742,6 +697,78 @@ class FastMCP( rewrite_tool_meta_for_wire(t) if _is_prefab_tool(t) else t for t in tools ] + async def _rebind_prefab_tool_names(self, result: Any) -> Any: + """Re-address a Prefab payload's tool references to this server's names. + + Runs on the way out of every ``tools/call``, above the middleware + chain so a payload is re-addressed however it was produced. Servers + unwind innermost-first, so the outermost server rewrites last and its + names — the only ones a client can actually invoke — are what ship. + + A call does not always answer with a tool result: submitting a task + answers with the task's metadata. Anything that is not a tool result + passes through untouched. + + An identity claimed by more than one tool is not bound. That happens + when one app is composed into a server twice, which leaves no fact + anywhere in the listing that says which copy a UI belongs to. The + reference keeps its identity-addressed form, and the dispatcher + reports the ambiguity rather than binding to a coin flip. + """ + from fastmcp.server.providers.prefab_payload import ( + payload_has_identities, + rewrite_payload_tool_names, + ) + + if not isinstance(result, ToolResult): + return result + + payload = result.structured_content + if not payload_has_identities(payload): + return result + + # Binding is safe only where one identity, one name, and one + # component all agree. Each is tracked separately: collapsing them + # early is what lets a duplicated app pass as a single tool. + # + # The middleware chain runs, because the binding has to describe the + # listing a client will actually see. Middleware adds, removes and + # shadows tools — an injected tool sharing a backend's name owns that + # name at call time, and a listing taken beneath middleware would not + # know it exists. + claimed_by: dict[str, list[Tool]] = {} + owners_of: dict[str, set[str | None]] = {} + for tool in await self.list_tools(): + identity = _tool_identity(tool) + owners_of.setdefault(tool.name, set()).add(identity) + if identity is not None: + claimed_by.setdefault(identity, []).append(tool) + + def resolve(identity: str) -> str | None: + tools = claimed_by.get(identity, []) + names = {tool.name for tool in tools} + if len(names) != 1: + # Several names carry this identity: the app is composed more + # than once and nothing says which copy the UI belongs to. + return None + + # One name can still be several components. `key` is the canonical + # identity — type, name and version — so versions of one tool have + # distinct keys while copies of one app repeat a key. A repeat + # means two components are indistinguishable, which is worse than + # the renamed case, not better. + if len({tool.key for tool in tools}) != len(tools): + return None + + (name,) = names + # And the name has to lead back. Two apps can each expose `save`, + # or a plain tool can share the name — binding then hands one + # app's button to someone else's implementation. + return name if owners_of.get(name) == {identity} else None + + rewrite_payload_tool_names(payload, resolve) + return result + # ------------------------------------------------------------------------- # Provider interface overrides - inherited from AggregateProvider # ------------------------------------------------------------------------- @@ -800,7 +827,7 @@ class FastMCP( async def list_tools(self, *, run_middleware: bool = True) -> Sequence[Tool]: """List all enabled tools from providers. - Overrides Provider.list_tools() to add visibility filtering, auth filtering, + Overrides Provider.list_tools() to add enabled filtering, auth filtering, and middleware execution. Returns all versions (no deduplication). Protocol handlers deduplicate for MCP wire format. """ @@ -820,11 +847,14 @@ class FastMCP( # Core logic: list tools with server_span("tools/list", "tools/list", self.name, "tool", ""): - # Get all tools, apply session transforms, then filter enabled - # and model-visible (app-only tools are hidden from the model). + # Get all tools, apply session transforms, then filter enabled. + # App-only tools (meta.ui.visibility == ["app"]) are listed: + # the mcp-apps spec puts visibility filtering on the host, and + # a tool absent from tools/list cannot be forwarded by any + # intermediary that routes by name. tools = list(await super().list_tools()) tools = await apply_session_transforms(tools) - tools = [t for t in tools if is_enabled(t) and not _is_backend_tool(t)] + tools = [t for t in tools if is_enabled(t)] # Rewrite per-tool Prefab renderer URIs based on the tool's # mount-point address. The walk pairs each tool with the @@ -882,7 +912,7 @@ class FastMCP( ) -> Tool | None: """Get a tool by name, filtering disabled tools. - Overrides Provider.get_tool() to add visibility filtering after all + Overrides Provider.get_tool() to filter disabled tools after all transforms (including session-level) have been applied. This ensures session transforms can override provider-level disables. @@ -902,18 +932,18 @@ class FastMCP( # Apply session transforms to single item tools = await apply_session_transforms([tool]) - if tools and is_enabled(tools[0]) and not _is_backend_tool(tools[0]): + if tools and is_enabled(tools[0]): return tools[0] - # The highest version is disabled (or app-only). If an explicit version - # was requested, respect that. Otherwise fall back to the next-highest - # enabled, model-visible version. + # The highest version is disabled. If an explicit version was + # requested, respect that. Otherwise fall back to the next-highest + # enabled version. if version is not None: return None all_tools = [t for t in await super().list_tools() if t.name == name] all_tools = list(await apply_session_transforms(all_tools)) - enabled = [t for t in all_tools if is_enabled(t) and not _is_backend_tool(t)] + enabled = [t for t in all_tools if is_enabled(t)] skip_auth, token = _get_auth_context() authorized: list[Tool] = [] @@ -1398,7 +1428,7 @@ class FastMCP( # the whole thing (so it observes every call), and the # interceptors sit between it and the tool body (so each is the # last gate before execution). - return await self._dispatch_component_middleware( + dispatched = await self._dispatch_component_middleware( context=mw_context, call_next=self._compose_tool_call_interceptors( lambda context: self.call_tool( @@ -1409,6 +1439,10 @@ class FastMCP( ) ), ) + # Above the chain, so a Prefab payload is re-addressed however + # it was produced — middleware can answer a call itself, and + # such a result never reaches the core path below. + return await self._rebind_prefab_tool_names(dispatched) # Core logic: find and execute tool with server_span( diff --git a/fastmcp_slim/fastmcp/tools/base.py b/fastmcp_slim/fastmcp/tools/base.py index fcf5e7e18..ad21f1253 100644 --- a/fastmcp_slim/fastmcp/tools/base.py +++ b/fastmcp_slim/fastmcp/tools/base.py @@ -518,15 +518,17 @@ def _get_tool_resolver(app_name: str | None = None) -> Callable[..., str] | None def _prefab_to_json(app: Any, fastmcp_app_name: str | None = None) -> dict[str, Any]: - """Call PrefabApp.to_json() with the hash-based resolver. + """Serialize a PrefabApp, addressing its peer-tool references by identity. - The resolver prefixes peer-tool references with a deterministic hash - derived from the app name + tool name. The dispatcher recognizes that - format and routes calls via ``get_tool_by_hash`` which walks the - provider tree recursively — same pattern as the old ``get_app_tool``. + The resolver writes each reference as ``_``, and the + identity behind it is recorded in the payload's meta so that servers + can re-address the reference on the way out without losing track of + what it points at. """ + from fastmcp.server.providers.prefab_payload import annotate_payload_identities + data = app.to_json(tool_resolver=_get_tool_resolver(fastmcp_app_name)) - return data + return annotate_payload_identities(data) def _get_fastmcp_app_name(tool: Tool) -> str | None: diff --git a/fastmcp_slim/fastmcp/tools/tool_transform.py b/fastmcp_slim/fastmcp/tools/tool_transform.py index effb5386b..2ac0a2dc3 100644 --- a/fastmcp_slim/fastmcp/tools/tool_transform.py +++ b/fastmcp_slim/fastmcp/tools/tool_transform.py @@ -242,6 +242,50 @@ class ArgTransformConfig(FastMCPBaseModel): return ArgTransform(**self.model_dump(exclude_unset=True)) # pyright: ignore[reportAny] +#: Meta namespaces the framework owns. An override replaces the caller-facing +#: meta wholesale, but these carry a component's app membership, identity, and +#: visibility — what intermediaries use to recognize a tool they are +#: forwarding. Both are needed together: an identity that survives a rename +#: while its ``ui.visibility`` marker does not leaves a tool that can be named +#: but no longer answers to its identity. +_FRAMEWORK_META_NAMESPACES = ("fastmcp", "ui") + + +def _apply_meta_override( + source_meta: dict[str, Any] | None, + override: dict[str, Any] | None | NotSetT, +) -> dict[str, Any] | None: + """Apply a transform's ``meta=`` override, preserving framework namespaces. + + An override replaces the caller-facing meta wholesale, which is what users + expect. Framework-owned namespaces are carried across regardless, since a + transform that renames a tool must not silently unwire it — values the + override supplies for those namespaces still win key by key. + """ + if isinstance(override, NotSetT): + return source_meta + + source = source_meta or {} + preserved = { + namespace: dict(source[namespace]) + for namespace in _FRAMEWORK_META_NAMESPACES + if isinstance(source.get(namespace), dict) and source[namespace] + } + + if override is None: + return preserved or None + + merged = dict(override) + for namespace, source_values in preserved.items(): + override_values = override.get(namespace) + merged[namespace] = ( + {**source_values, **override_values} + if isinstance(override_values, dict) + else source_values + ) + return merged + + class TransformedTool(Tool): """A tool that is transformed from another tool. @@ -590,7 +634,7 @@ class TransformedTool(Tool): description if not isinstance(description, NotSetT) else tool.description ) final_title = title if not isinstance(title, NotSetT) else tool.title - final_meta = meta if not isinstance(meta, NotSetT) else tool.meta + final_meta = _apply_meta_override(tool.meta, meta) final_annotations = ( annotations if not isinstance(annotations, NotSetT) else tool.annotations ) diff --git a/tests/apps/test_file_upload.py b/tests/apps/test_file_upload.py index f8b65538b..e30646846 100644 --- a/tests/apps/test_file_upload.py +++ b/tests/apps/test_file_upload.py @@ -141,7 +141,9 @@ class TestFileUploadProvider: text = result.content[0].text # type: ignore[union-attr] # ty:ignore[unresolved-attribute] assert "test.txt" in text - async def test_ui_tool_visible_backend_hidden(self): + async def test_backend_tool_listed_as_app_only(self): + """``store_files`` is listed but declares visibility=["app"], so the + host keeps it out of the model's tool list.""" server = FastMCP("test", providers=[FileUpload()]) tools = await server.list_tools() @@ -150,7 +152,11 @@ class TestFileUploadProvider: assert "file_manager" in tool_names assert "list_files" in tool_names assert "read_file" in tool_names - assert "store_files" not in tool_names + assert "store_files" in tool_names + + store_files = next(t for t in tools if t.name == "store_files") + assert store_files.meta is not None + assert store_files.meta["ui"]["visibility"] == ["app"] async def test_max_file_size_enforced_server_side(self): server = FastMCP("test", providers=[FileUpload(max_file_size=100)]) diff --git a/tests/server/providers/test_prefab_roundtrip.py b/tests/server/providers/test_prefab_roundtrip.py index 72946910f..05897eef8 100644 --- a/tests/server/providers/test_prefab_roundtrip.py +++ b/tests/server/providers/test_prefab_roundtrip.py @@ -8,22 +8,46 @@ single-server, namespaced mounts, and cross-server mounts. from __future__ import annotations -import json - import pytest from fastmcp import FastMCP, FastMCPApp -from fastmcp.server.providers.addressing import hashed_backend_name +from fastmcp.exceptions import ToolError +from fastmcp.experimental.transforms.code_mode import CodeMode +from fastmcp.server.middleware.tool_injection import ToolInjectionMiddleware +from fastmcp.server.providers.addressing import hash_tool, hashed_backend_name +from fastmcp.server.providers.proxy import ProxyClient, ProxyProvider +from fastmcp.server.transforms.search import RegexSearchTransform +from fastmcp.server.transforms.tool_transform import ToolTransform +from fastmcp.tools.base import Tool +from fastmcp.tools.tool_transform import ToolTransformConfig prefab_ui = pytest.importorskip("prefab_ui") from prefab_ui.actions.mcp import CallTool # noqa: E402 from prefab_ui.components import Button, Column, Text # noqa: E402 +def _tool_refs(payload) -> list[str]: + """Every tool name the rendered UI would call, in document order.""" + refs: list[str] = [] + + def walk(node) -> None: + if isinstance(node, dict): + if node.get("action") == "toolCall" and isinstance(node.get("tool"), str): + refs.append(node["tool"]) + for value in node.values(): + walk(value) + elif isinstance(node, list): + for item in node: + walk(item) + + walk(payload) + return refs + + class TestSingleServerRoundTrip: - async def test_ui_tool_serializes_hashed_peer_reference(self): - """The resolver converts a CallTool string reference to a hashed - name that appears in the tool result's structured_content.""" + async def test_payload_carries_the_servers_own_tool_name(self): + """The renderer is handed a name that exists in this server's + tools/list, not the identity-addressed form.""" app = FastMCPApp("contacts") @app.tool() @@ -41,13 +65,32 @@ class TestSingleServerRoundTrip: result = await server.call_tool("contact_form", {}) assert result.structured_content is not None + assert _tool_refs(result.structured_content) == ["save_contact"] - # The hashed name should appear somewhere in the serialized output. - sc_json = json.dumps(result.structured_content) - expected_hash = hashed_backend_name("contacts", "save_contact") - assert expected_hash in sc_json, ( - f"Expected {expected_hash!r} in structured_content but got: {sc_json[:200]}" - ) + async def test_payload_records_the_identity_behind_each_reference(self): + """The identity-addressed form survives alongside the rewritten name, + so an outer server can re-resolve it — or fall back to it.""" + app = FastMCPApp("contacts") + + @app.tool() + def save_contact(name: str) -> str: + return f"saved {name}" + + @app.ui() + def contact_form() -> Column: + return Column( + children=[Button(label="Save", on_click=CallTool(tool="save_contact"))] + ) + + server = FastMCP("Platform") + server.add_provider(app) + + result = await server.call_tool("contact_form", {}) + assert result.structured_content is not None + names = result.structured_content["_meta"]["fastmcp"]["toolNames"] + assert names == { + "save_contact": hashed_backend_name("contacts", "save_contact") + } async def test_hashed_name_from_result_is_callable(self): """The hashed name that appears in structured_content actually @@ -131,6 +174,470 @@ class TestMountedServerRoundTrip: assert result.content[0].text == "saved Carol" # type: ignore[union-attr] # ty:ignore[unresolved-attribute] +class TestProxiedServerRoundTrip: + """A gateway proxying an app-bearing backend. + + A proxy knows only what crossed the wire, so this is the topology that + breaks if app-only tools are filtered out of tools/list or if the + identity hash is stripped from meta. + """ + + @staticmethod + def _backend() -> FastMCP: + app = FastMCPApp("contacts") + + @app.tool() + def save(name: str) -> str: + return f"saved {name}" + + @app.ui() + def form() -> Text: + return Text(content="Form") + + backend = FastMCP("Backend") + backend.add_provider(app) + return backend + + async def test_app_only_tool_is_forwarded_through_a_proxy(self): + backend = self._backend() + gateway = FastMCP("Gateway") + gateway.add_provider(ProxyProvider(lambda: ProxyClient(backend))) + + names = [t.name for t in await gateway.list_tools()] + assert "save" in names + + async def test_identity_hash_survives_the_proxy(self): + backend = self._backend() + gateway = FastMCP("Gateway") + gateway.add_provider(ProxyProvider(lambda: ProxyClient(backend))) + + tool = next(t for t in await gateway.list_tools() if t.name == "save") + assert tool.meta is not None + assert tool.meta["fastmcp"]["tool_hash"] == hash_tool("contacts", "save") + + async def test_backend_tool_callable_by_hash_through_a_proxy(self): + backend = self._backend() + gateway = FastMCP("Gateway") + gateway.add_provider(ProxyProvider(lambda: ProxyClient(backend))) + + hashed_name = hashed_backend_name("contacts", "save") + result = await gateway.call_tool(hashed_name, {"name": "Dana"}) + assert result.content[0].text == "saved Dana" # type: ignore[union-attr] # ty:ignore[unresolved-attribute] + + async def test_backend_tool_callable_through_a_namespaced_proxy(self): + backend = self._backend() + gateway = FastMCP("Gateway") + gateway.add_provider( + ProxyProvider(lambda: ProxyClient(backend)), namespace="up" + ) + + names = [t.name for t in await gateway.list_tools()] + assert "up_save" in names + + hashed_name = hashed_backend_name("contacts", "save") + result = await gateway.call_tool(hashed_name, {"name": "Erin"}) + assert result.content[0].text == "saved Erin" # type: ignore[union-attr] # ty:ignore[unresolved-attribute] + + async def test_backend_tool_callable_through_chained_proxies(self): + backend = self._backend() + middle = FastMCP("Middle") + middle.add_provider(ProxyProvider(lambda: ProxyClient(backend))) + top = FastMCP("Top") + top.add_provider(ProxyProvider(lambda: ProxyClient(middle))) + + hashed_name = hashed_backend_name("contacts", "save") + result = await top.call_tool(hashed_name, {"name": "Frank"}) + assert result.content[0].text == "saved Frank" # type: ignore[union-attr] # ty:ignore[unresolved-attribute] + + +class TestLateBoundToolNames: + """The payload is re-addressed on the way out of every FastMCP server. + + Servers unwind innermost-first, so the outermost one rewrites last and its + names — the only ones a client can invoke — are what the renderer receives. + """ + + @staticmethod + def _app(marker: str = "x", app_name: str = "contacts") -> FastMCPApp: + app = FastMCPApp(app_name) + + @app.tool() + def save(name: str) -> str: + return f"[{marker}] saved {name}" + + @app.ui() + def form() -> Column: + return Column( + children=[Button(label="Save", on_click=CallTool(tool="save"))] + ) + + return app + + async def test_namespaced_server_emits_its_namespaced_name(self): + server = FastMCP("Platform") + server.add_provider(self._app(), namespace="crm") + + result = await server.call_tool("crm_form", {}) + assert _tool_refs(result.structured_content) == ["crm_save"] + + async def test_name_accumulates_through_nested_mounts(self): + inner = FastMCP("Inner") + inner.add_provider(self._app(), namespace="a") + mid = FastMCP("Mid") + mid.add_provider(inner, namespace="b") + top = FastMCP("Top") + top.add_provider(mid, namespace="c") + + result = await top.call_tool("c_b_a_form", {}) + assert _tool_refs(result.structured_content) == ["c_b_a_save"] + + async def test_gateway_emits_its_own_name_not_the_backends(self): + backend = FastMCP("Backend") + backend.add_provider(self._app()) + + gateway = FastMCP("Gateway") + gateway.add_provider( + ProxyProvider(lambda: ProxyClient(backend)), namespace="up" + ) + + result = await gateway.call_tool("up_form", {}) + assert _tool_refs(result.structured_content) == ["up_save"] + + async def test_emitted_name_is_callable_on_the_same_server(self): + """The whole point: what the renderer is told to call, it can call.""" + backend = FastMCP("Backend") + backend.add_provider(self._app(marker="be")) + + gateway = FastMCP("Gateway") + gateway.add_provider( + ProxyProvider(lambda: ProxyClient(backend)), namespace="up" + ) + + result = await gateway.call_tool("up_form", {}) + (ref,) = _tool_refs(result.structured_content) + assert ref in [t.name for t in await gateway.list_tools()] + + clicked = await gateway.call_tool(ref, {"name": "alice"}) + assert clicked.content[0].text == "[be] saved alice" # type: ignore[union-attr] # ty:ignore[unresolved-attribute] + + @pytest.mark.parametrize( + "transform_factory,expected_listing", + [ + ( + lambda: RegexSearchTransform(), + ["search_tools", "call_tool"], + ), + ( + lambda: CodeMode(), + ["search", "get_schema", "execute"], + ), + ], + ids=["tool-search", "code-mode"], + ) + async def test_survives_a_collapsed_catalog( + self, transform_factory, expected_listing + ): + """Tool search and code mode replace tools/list wholesale, so there is + no better name to bind to. The reference stays identity-addressed and + the hashed path still resolves it.""" + server = FastMCP("Platform") + server.add_provider(self._app(marker="cat")) + server.add_transform(transform_factory()) + + assert [t.name for t in await server.list_tools()] == expected_listing + + result = await server.call_tool("form", {}) + (ref,) = _tool_refs(result.structured_content) + assert ref == hashed_backend_name("contacts", "save") + + clicked = await server.call_tool(ref, {"name": "alice"}) + assert clicked.content[0].text == "[cat] saved alice" # type: ignore[union-attr] # ty:ignore[unresolved-attribute] + + @pytest.mark.parametrize( + "compose", + ["siblings", "nested", "prefixing-namespaces"], + ) + async def test_a_duplicated_app_is_not_bound(self, compose): + """One app composed twice leaves no fact in the listing saying which + copy a UI belongs to, so no name is bound and the reference keeps its + identity. Covers copies as siblings, nested inside one subtree, and + under namespaces that prefix one another. + """ + if compose == "nested": + inner = FastMCP("Inner") + inner.add_provider(self._app(marker="A"), namespace="a") + inner.add_provider(self._app(marker="B"), namespace="b") + server = FastMCP("Top") + server.add_provider(inner, namespace="outer") + entry = "outer_a_form" + else: + second = "a_form" if compose == "prefixing-namespaces" else "b" + server = FastMCP("Top") + server.add_provider(self._app(marker="A"), namespace="a") + server.add_provider(self._app(marker="B"), namespace=second) + entry = "a_form" + + result = await server.call_tool(entry, {}) + (ref,) = _tool_refs(result.structured_content) + assert ref == hashed_backend_name("contacts", "save") + + async def test_a_duplicated_app_reports_the_ambiguity(self): + """The unbound reference must fail with a message that names the real + cause, at any depth — a nested duplicate previously surfaced as + `Unknown tool`, sending readers after a missing registration. + """ + inner = FastMCP("Inner") + inner.add_provider(self._app(marker="A"), namespace="a") + inner.add_provider(self._app(marker="B"), namespace="b") + server = FastMCP("Top") + server.add_provider(inner, namespace="outer") + + result = await server.call_tool("outer_a_form", {}) + (ref,) = _tool_refs(result.structured_content) + + with pytest.raises(ToolError, match="composed more than once"): + await server.call_tool(ref, {"name": "alice"}) + + @pytest.mark.parametrize("backend_namespace", [None, "crm"]) + async def test_collapsed_catalog_over_a_proxy(self, backend_namespace): + """The collapsed-catalog fallback has to survive a backend that + renamed its app tools. Nothing named `save` was ever listed across + the wire, so the identity has to resolve against the remote listing + rather than against a name that only exists at the origin. + """ + app = self._app(marker="be") + backend = FastMCP("Backend") + backend.add_provider(app, namespace=backend_namespace) + + gateway = FastMCP("Gateway") + gateway.add_provider(ProxyProvider(lambda: ProxyClient(backend))) + gateway.add_transform(RegexSearchTransform()) + + entry = f"{backend_namespace}_form" if backend_namespace else "form" + result = await gateway.call_tool(entry, {}) + (ref,) = _tool_refs(result.structured_content) + assert ref == hashed_backend_name("contacts", "save") + + clicked = await gateway.call_tool(ref, {"name": "alice"}) + assert clicked.content[0].text == "[be] saved alice" # type: ignore[union-attr] # ty:ignore[unresolved-attribute] + + async def test_versions_of_one_tool_are_a_single_target(self): + """Versions are listed individually and share an identity, but they + also share a name that resolves to the highest version on its own. + Only distinct names mean distinct copies of an app. + """ + app = FastMCPApp("contacts") + for version, prefix in (("1.0.0", "v1"), ("2.0.0", "v2")): + + def save(name: str, _prefix: str = prefix) -> str: + return f"{_prefix} saved {name}" + + app.add_tool(Tool.from_function(save, name="save", version=version)) + + @app.ui() + def form() -> Column: + return Column( + children=[Button(label="Save", on_click=CallTool(tool="save"))] + ) + + server = FastMCP("Platform") + server.add_provider(app) + + result = await server.call_tool("form", {}) + (ref,) = _tool_refs(result.structured_content) + assert ref == "save" + + clicked = await server.call_tool(ref, {"name": "alice"}) + assert clicked.content[0].text == "v2 saved alice" # type: ignore[union-attr] # ty:ignore[unresolved-attribute] + + async def test_distinct_apps_sharing_a_backend_name(self): + """Identity and name must agree in both directions. Two apps can each + expose `save`: the identities differ and each has one candidate, but + the shared name resolves to only one of them. + """ + server = FastMCP("Platform") + for app_name, entry, marker in ( + ("crm", "crm_ui", "CRM"), + ("billing", "billing_ui", "BILLING"), + ): + app = FastMCPApp(app_name) + + @app.tool() + def save(name: str, _marker: str = marker) -> str: + return f"[{_marker}] saved {name}" + + @app.ui(entry) + def form() -> Column: + return Column( + children=[Button(label="Save", on_click=CallTool(tool="save"))] + ) + + server.add_provider(app) + + result = await server.call_tool("billing_ui", {}) + (ref,) = _tool_refs(result.structured_content) + assert ref == hashed_backend_name("billing", "save") + + async def test_proxy_refuses_a_remote_that_duplicates_an_app(self): + """A remote mounting one app twice sends back two tools claiming one + identity, and the proxy must refuse on the same terms a local + composition would rather than returning whichever came first. + """ + backend = FastMCP("Backend") + backend.add_provider(self._app(marker="A"), namespace="a") + backend.add_provider(self._app(marker="B"), namespace="b") + + gateway = FastMCP("Gateway") + gateway.add_provider(ProxyProvider(lambda: ProxyClient(backend))) + + with pytest.raises(ToolError, match="composed more than once"): + await gateway.call_tool( + hashed_backend_name("contacts", "save"), {"name": "alice"} + ) + + async def test_middleware_owns_the_names_it_shadows(self): + """Binding describes the listing a client will see, so it has to run + the middleware chain. An injected tool sharing a backend's name owns + that name at call time, and would be invisible to a listing taken + beneath middleware. + """ + app = FastMCPApp("contacts") + + @app.tool() + def save(name: str) -> str: + return f"[APP] saved {name}" + + @app.ui() + def form() -> Column: + return Column( + children=[Button(label="Save", on_click=CallTool(tool="save"))] + ) + + def injected(name: str) -> str: + return f"[INJECTED] saved {name}" + + server = FastMCP("Platform") + server.add_provider(app) + server.add_middleware( + ToolInjectionMiddleware([Tool.from_function(injected, name="save")]) + ) + + result = await server.call_tool("form", {}) + (ref,) = _tool_refs(result.structured_content) + assert ref == hashed_backend_name("contacts", "save") + + clicked = await server.call_tool(ref, {"name": "alice"}) + assert clicked.content[0].text == "[APP] saved alice" # type: ignore[union-attr] # ty:ignore[unresolved-attribute] + + async def test_middleware_produced_results_are_rebound(self): + """Middleware can answer a call itself, and such a result never + reaches the core dispatch path — so rebinding belongs above the + chain, not inside it. + """ + app = FastMCPApp("contacts") + + @app.tool() + def save(name: str) -> str: + return f"saved {name}" + + @app.ui() + def form() -> Column: + return Column( + children=[Button(label="Save", on_click=CallTool(tool="save"))] + ) + + server = FastMCP("Platform") + server.add_provider(app) + + entry = await server.get_tool("form") + assert entry is not None + server.add_middleware( + ToolInjectionMiddleware([entry.model_copy(update={"name": "injected"})]) + ) + + result = await server.call_tool("injected", {}) + (ref,) = _tool_refs(result.structured_content) + assert ref == "save" + + clicked = await server.call_tool(ref, {"name": "alice"}) + assert clicked.content[0].text == "saved alice" # type: ignore[union-attr] # ty:ignore[unresolved-attribute] + + async def test_a_transform_cannot_unwire_an_app_tool(self): + """A meta override that keeps the identity but drops app visibility + leaves a tool that can be named yet no longer answers to its + identity — which is the only address a collapsed catalog has. + """ + backend = FastMCP("Backend") + backend.add_provider(self._app(marker="be")) + backend.add_transform( + ToolTransform({"save": ToolTransformConfig(meta={"team": "crm"})}) + ) + + transformed = next(t for t in await backend.list_tools() if t.name == "save") + assert transformed.meta is not None + assert transformed.meta["ui"]["visibility"] == ["app"] + assert transformed.meta["team"] == "crm" + + gateway = FastMCP("Gateway") + gateway.add_provider(ProxyProvider(lambda: ProxyClient(backend))) + gateway.add_transform(RegexSearchTransform()) + + result = await gateway.call_tool("form", {}) + (ref,) = _tool_refs(result.structured_content) + assert ref == hashed_backend_name("contacts", "save") + + clicked = await gateway.call_tool(ref, {"name": "alice"}) + assert clicked.content[0].text == "[be] saved alice" # type: ignore[union-attr] # ty:ignore[unresolved-attribute] + + async def test_duplicate_copies_are_not_collapsed_by_a_shared_name(self): + """Copies whose backends collide on a name are the worst case, not the + safe one: two components become indistinguishable. Counting names + alone would see a single unambiguous target and bind to it. + """ + server = FastMCP("Platform") + for entry, marker in (("form_a", "A"), ("form_b", "B")): + app = FastMCPApp("contacts") + + @app.tool() + def save(name: str, _marker: str = marker) -> str: + return f"[{_marker}] saved {name}" + + @app.ui(entry) + def form() -> Column: + return Column( + children=[Button(label="Save", on_click=CallTool(tool="save"))] + ) + + server.add_provider(app) + + listed = await server.list_tools() + assert [t.key for t in listed].count("tool:save@") == 2 + + result = await server.call_tool("form_b", {}) + (ref,) = _tool_refs(result.structured_content) + assert ref == hashed_backend_name("contacts", "save") + + async def test_unresolvable_identity_is_restored(self): + """An inner server binds to a name that means nothing further out, so + a reference this server cannot resolve is restored to its identity + rather than left — a stranded name has no route back, an identity does. + """ + app = FastMCPApp("contacts") + + @app.ui() + def form() -> Column: + return Column( + children=[Button(label="Go", on_click=CallTool(tool="not_registered"))] + ) + + server = FastMCP("Platform") + server.add_provider(app) + + result = await server.call_tool("form", {}) + (ref,) = _tool_refs(result.structured_content) + assert ref == hashed_backend_name("contacts", "not_registered") + + class TestDynamicToolAdd: async def test_tool_added_after_first_call_is_reachable(self): """Tools added to an already-mounted app after the first call @@ -157,10 +664,9 @@ class TestDynamicToolAdd: class TestCollision: - async def test_same_app_name_same_tool_name_first_wins(self): - """Two apps with the same name and same tool name: the hash is - identical, so get_tool_by_hash returns the first match. This is - the same first-match behavior the old get_app_tool had.""" + async def test_distinct_hashes_resolve_independently(self): + """Two apps sharing a name but with different tool names hash + differently, so each tool resolves to itself.""" app_a = FastMCPApp("shared") app_b = FastMCPApp("shared") @@ -172,14 +678,67 @@ class TestCollision: def save_b(name: str) -> str: return f"from B: {name}" - # Register under a different local tool name to avoid - # actual collision at the provider level. The hash collision - # only happens when both app name AND tool name match. - # This test just verifies one app's tool is reachable. server = FastMCP("Platform") server.add_provider(app_a) server.add_provider(app_b) - hashed_name = hashed_backend_name("shared", "save") - result = await server.call_tool(hashed_name, {"name": "Eve"}) + result = await server.call_tool( + hashed_backend_name("shared", "save"), {"name": "Eve"} + ) assert result.content[0].text == "from A: Eve" # type: ignore[union-attr] # ty:ignore[unresolved-attribute] + + result_b = await server.call_tool( + hashed_backend_name("shared", "save_b"), {"name": "Eve"} + ) + assert result_b.content[0].text == "from B: Eve" # type: ignore[union-attr] # ty:ignore[unresolved-attribute] + + async def test_ambiguous_identity_raises_rather_than_guessing(self): + """The same app composed into two branches yields two tools with one + identity. Routing to either would silently execute the wrong branch's + tool, so the call is refused.""" + server = FastMCP("Platform") + for marker, namespace in (("A", "a"), ("B", "b")): + app = FastMCPApp("contacts") + + @app.tool() + def save(name: str, _marker: str = marker) -> str: + return f"from {_marker}: {name}" + + server.add_provider(app, namespace=namespace) + + with pytest.raises(ToolError, match="Ambiguous app tool"): + await server.call_tool( + hashed_backend_name("contacts", "save"), {"name": "Eve"} + ) + + async def test_distinct_app_names_route_independently_through_a_gateway(self): + """The multi-tenant gateway shape: distinct app names stay unambiguous + no matter how many backends sit behind one proxy.""" + + def backend(marker: str, app_name: str) -> FastMCP: + app = FastMCPApp(app_name) + + @app.tool() + def save(name: str) -> str: + return f"from {marker}: {name}" + + server = FastMCP(f"Backend-{marker}") + server.add_provider(app) + return server + + first = backend("A", "crm") + second = backend("B", "billing") + + gateway = FastMCP("Gateway") + gateway.add_provider(ProxyProvider(lambda: ProxyClient(first)), namespace="a") + gateway.add_provider(ProxyProvider(lambda: ProxyClient(second)), namespace="b") + + result_a = await gateway.call_tool( + hashed_backend_name("crm", "save"), {"name": "Eve"} + ) + assert result_a.content[0].text == "from A: Eve" # type: ignore[union-attr] # ty:ignore[unresolved-attribute] + + result_b = await gateway.call_tool( + hashed_backend_name("billing", "save"), {"name": "Eve"} + ) + assert result_b.content[0].text == "from B: Eve" # type: ignore[union-attr] # ty:ignore[unresolved-attribute] diff --git a/tests/test_fastmcp_app.py b/tests/test_fastmcp_app.py index 86f9eae60..9737454ff 100644 --- a/tests/test_fastmcp_app.py +++ b/tests/test_fastmcp_app.py @@ -22,6 +22,7 @@ from fastmcp.apps.app import ( FastMCPApp, _make_resolver, ) +from fastmcp.server.providers.addressing import hash_tool, hashed_backend_name from fastmcp.tools.base import Tool # --------------------------------------------------------------------------- @@ -579,13 +580,13 @@ class TestCallToolAppRouting: # --------------------------------------------------------------------------- -# App-only tool filtering from server list_tools / get_tool +# App-only tool visibility: declared in meta, listed on the wire # --------------------------------------------------------------------------- -class TestAppOnlyToolFiltering: - async def test_app_only_tool_hidden_from_list_tools(self): - """@app.tool() (visibility=["app"]) should not appear in server.list_tools().""" +class TestAppOnlyToolVisibility: + async def test_app_only_tool_appears_in_list_tools(self): + """@app.tool() (visibility=["app"]) is listed; the host filters it out.""" app = FastMCPApp("crm") @app.tool() @@ -597,7 +598,40 @@ class TestAppOnlyToolFiltering: tools = await server.list_tools() names = [t.name for t in tools] - assert "save_contact" not in names + assert "save_contact" in names + + async def test_app_only_tool_declares_app_visibility(self): + """The listed tool carries visibility=["app"] so a host can filter it.""" + app = FastMCPApp("crm") + + @app.tool() + def save_contact(name: str) -> str: + return name + + server = FastMCP("Platform") + server.add_provider(app) + + tool = next(t for t in await server.list_tools() if t.name == "save_contact") + assert tool.meta is not None + assert tool.meta["ui"]["visibility"] == ["app"] + + async def test_app_only_tool_visibility_survives_the_wire(self): + """A client sees the visibility declaration, which is what it filters on.""" + app = FastMCPApp("crm") + + @app.tool() + def save_contact(name: str) -> str: + return name + + server = FastMCP("Platform") + server.add_provider(app) + + async with Client(server) as client: + tool = next( + t for t in await client.list_tools() if t.name == "save_contact" + ) + assert tool.meta is not None + assert tool.meta["ui"]["visibility"] == ["app"] async def test_model_visible_tool_in_list_tools(self): """@app.tool(model=True) (visibility=["app","model"]) appears in list_tools.""" @@ -629,8 +663,8 @@ class TestAppOnlyToolFiltering: names = [t.name for t in tools] assert "show_dashboard" in names - async def test_app_only_tool_still_callable_via_app_name(self): - """Even though filtered from list_tools, app-only tools are callable via call_tool with app_name.""" + async def test_app_only_tool_callable_via_hashed_address(self): + """The hashed address still resolves, independent of the display name.""" app = FastMCPApp("contacts") @app.tool() @@ -640,35 +674,30 @@ class TestAppOnlyToolFiltering: server = FastMCP("Platform") server.add_provider(app) - # Verify it's hidden from list_tools - tools = await server.list_tools() - names = [t.name for t in tools] - assert "save" not in names - - # But still callable via the hashed-address routing path. - from fastmcp.server.providers.addressing import hashed_backend_name - result = await server.call_tool( hashed_backend_name("contacts", "save"), {"name": "alice"} ) assert result.content[0].text == "saved alice" # type: ignore[union-attr] # ty:ignore[unresolved-attribute] - async def test_app_only_tool_hidden_from_get_tool(self): - """server.get_tool() returns None for app-only tools.""" - app = FastMCPApp("crm") + async def test_app_only_tool_callable_by_display_name(self): + """App-only tools resolve normally; the host decides who may call them.""" + app = FastMCPApp("contacts") @app.tool() - def save_contact(name: str) -> str: - return name + def save(name: str) -> str: + return f"saved {name}" server = FastMCP("Platform") server.add_provider(app) - tool = await server.get_tool("save_contact") - assert tool is None + tool = await server.get_tool("save") + assert tool is not None - async def test_app_only_tool_hidden_with_namespace(self): - """App-only tools hidden even when accessed through a namespace.""" + result = await server.call_tool("save", {"name": "alice"}) + assert result.content[0].text == "saved alice" # type: ignore[union-attr] # ty:ignore[unresolved-attribute] + + async def test_app_only_tool_namespaced_in_list_tools(self): + """Namespacing renames app-only tools like any other tool.""" app = FastMCPApp("crm") @app.tool() @@ -680,7 +709,23 @@ class TestAppOnlyToolFiltering: tools = await server.list_tools() names = [t.name for t in tools] - assert "crm_save" not in names + assert "crm_save" in names + + async def test_app_only_tool_carries_public_hash(self): + """The identity hash is public meta, so intermediaries can match on it.""" + app = FastMCPApp("crm") + + @app.tool() + def save(name: str) -> str: + return name + + server = FastMCP("Platform") + server.add_provider(app, namespace="crm") + + async with Client(server) as client: + tool = next(t for t in await client.list_tools() if t.name == "crm_save") + assert tool.meta is not None + assert tool.meta["fastmcp"]["tool_hash"] == hash_tool("crm", "save") # --------------------------------------------------------------------------- @@ -872,21 +917,25 @@ class TestAppIntegration: server = FastMCP("Platform") server.add_provider(app, namespace="crm") - # The @app.ui() tool should be visible (namespaced) to the client. - # The @app.tool() backend tool should NOT appear. + # Both tools are listed (namespaced). The backend tool declares + # visibility=["app"] so the host keeps it out of the model's list. async with Client(server) as client: tools = await client.list_tools() tool_names = [t.name for t in tools] assert "crm_contact_form" in tool_names - assert "crm_save_contact" not in tool_names + assert "crm_save_contact" in tool_names + + backend = next(t for t in tools if t.name == "crm_save_contact") + assert backend.meta is not None + assert backend.meta["ui"]["visibility"] == ["app"] # Call the UI tool through the client and check structured_content result = await client.call_tool_mcp("crm_contact_form", {}) sc = result.structured_content assert sc is not None - # Call the backend tool via its hashed address — bypasses namespace - # transforms and visibility filtering by going through the registry. + # Call the backend tool via its hashed address — resolves regardless + # of the namespace transform applied to the display name. backend_result = await server.call_tool( hashed_backend_name("contacts", "save_contact"), {"name": "Alice", "email": "alice@example.com"}, diff --git a/tests/tools/tool_transform/test_metadata.py b/tests/tools/tool_transform/test_metadata.py index 36384afec..543339018 100644 --- a/tests/tools/tool_transform/test_metadata.py +++ b/tests/tools/tool_transform/test_metadata.py @@ -172,6 +172,45 @@ def test_tool_transform_config_removes_meta(sample_tool): assert transformed.meta is None +def test_meta_override_preserves_fastmcp_namespace(sample_tool): + """A meta override replaces caller meta but keeps framework-owned data. + + The fastmcp namespace carries app membership and the identity hash that + intermediaries match on. A rename via config must not destroy it. + """ + sample_tool.meta = {"original": True, "fastmcp": {"app": "crm", "tool_hash": "abc"}} + transformed = Tool.from_tool(sample_tool, meta={"custom": True}) + assert transformed.meta == { + "custom": True, + "fastmcp": {"app": "crm", "tool_hash": "abc"}, + } + + +def test_meta_none_preserves_fastmcp_namespace(sample_tool): + """Clearing meta clears caller meta, not the framework namespace.""" + sample_tool.meta = {"original": True, "fastmcp": {"app": "crm", "tool_hash": "abc"}} + transformed = Tool.from_tool(sample_tool, meta=None) + assert transformed.meta == {"fastmcp": {"app": "crm", "tool_hash": "abc"}} + + +def test_meta_override_can_extend_fastmcp_namespace(sample_tool): + """An override may add to the fastmcp namespace without dropping its keys.""" + sample_tool.meta = {"fastmcp": {"app": "crm", "tool_hash": "abc"}} + transformed = Tool.from_tool(sample_tool, meta={"fastmcp": {"extra": 1}}) + assert transformed.meta == { + "fastmcp": {"app": "crm", "tool_hash": "abc", "extra": 1} + } + + +def test_config_meta_override_preserves_identity_hash(sample_tool): + """The fastmcp.json `tools:` path goes through the same preservation.""" + sample_tool.meta = {"fastmcp": {"app": "crm", "tool_hash": "abc"}} + config = ToolTransformConfig(name="renamed", meta={"team": "growth"}) + transformed = config.apply(sample_tool) + assert transformed.meta is not None + assert transformed.meta["fastmcp"]["tool_hash"] == "abc" + + # Enabled field tests def test_tool_transform_config_enabled_defaults_to_true(sample_tool): """Test that enabled defaults to True and no visibility metadata is set.""" From 81b1e818e5f4f0a58085ca0c1a2c5f9ea64bc557 Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Tue, 28 Jul 2026 16:12:38 -0400 Subject: [PATCH 13/53] Apply app visibility where no host can (#4692) --- docs/apps/low-level.mdx | 4 +- docs/servers/transforms/tool-search.mdx | 2 + fastmcp_slim/fastmcp/apps/config.py | 29 +++ .../fastmcp/server/transforms/catalog.py | 14 +- .../fastmcp/server/transforms/search/base.py | 8 + .../test_model_visibility_boundary.py | 168 ++++++++++++++++++ 6 files changed, 223 insertions(+), 2 deletions(-) create mode 100644 tests/server/transforms/test_model_visibility_boundary.py diff --git a/docs/apps/low-level.mdx b/docs/apps/low-level.mdx index fc07e5b8d..46de3e03d 100644 --- a/docs/apps/low-level.mdx +++ b/docs/apps/low-level.mdx @@ -75,7 +75,9 @@ The `visibility` field controls where a tool appears: This is useful when you have tools that only make sense as part of the app's interactive flow, not as standalone LLM actions. -Visibility is a declaration, not server-side filtering. Every tool appears in `tools/list` carrying its `visibility` metadata, and the host decides what to show the model — the division the MCP Apps specification defines. Listing them is also what lets a proxy or gateway forward them: an intermediary can only route to a tool it can see. +Visibility is a declaration, and on `tools/list` the host does the filtering — the division the MCP Apps specification defines. Every tool is advertised carrying its `visibility` metadata, which is also what lets a proxy or gateway forward it: an intermediary can only route to a tool it can see. + +That division assumes a host stands between the server and the model. Where one doesn't, FastMCP applies the declaration itself. [Tool search](/servers/transforms/tool-search) and code mode reach the model as ordinary tool output rather than as an advertised listing, and their call-tool proxies execute a name the model supplies — nothing downstream can filter either, so app-only tools are excluded from both. The app's own UI still reaches its backends, because a UI calling by identity is not the model. ```python @mcp.tool( diff --git a/docs/servers/transforms/tool-search.mdx b/docs/servers/transforms/tool-search.mdx index 204004f5c..c3f44a3cf 100644 --- a/docs/servers/transforms/tool-search.mdx +++ b/docs/servers/transforms/tool-search.mdx @@ -153,6 +153,8 @@ Tools discovered through search can also be called directly via `client.call_too Search results respect the full authorization pipeline. Tools filtered by middleware, visibility transforms, or component-level auth checks won't appear in search results. +App-only tools are excluded too. A [MCP app](/apps/overview) can declare backend tools that only its UI may call, and normally the host keeps those from the model. A search result is tool output rather than an advertised listing, so no host filtering applies to it — the exclusion happens here instead. The `call_tool` proxy enforces the same boundary, since it executes a name the model supplies. + The search tool queries `list_tools()` through the complete pipeline at search time, so the same filtering that controls what a client sees in the listing also controls what they can discover through search. ```python diff --git a/fastmcp_slim/fastmcp/apps/config.py b/fastmcp_slim/fastmcp/apps/config.py index 3d89aa382..1686d8626 100644 --- a/fastmcp_slim/fastmcp/apps/config.py +++ b/fastmcp_slim/fastmcp/apps/config.py @@ -11,6 +11,7 @@ from typing import Any, Literal from pydantic import BaseModel, Field +from fastmcp.utilities.components import FastMCPComponent from fastmcp.utilities.mime import UI_MIME_TYPE as UI_MIME_TYPE from fastmcp.utilities.mime import resolve_ui_mime_type as resolve_ui_mime_type @@ -182,3 +183,31 @@ def app_config_to_meta_dict(app: AppConfig | dict[str, Any]) -> dict[str, Any]: if isinstance(app, AppConfig): return app.model_dump(by_alias=True, exclude_none=True) return app + + +def is_model_visible(component: FastMCPComponent) -> bool: + """Whether a component may be shown to, or invoked by, the model. + + Visibility is a declaration, and the MCP Apps spec puts the filtering on + the host — so ``tools/list`` carries app-only tools and the host keeps + them from the model. That division only works where a host stands between + the server and the model. + + It does not hold for surfaces a server drives itself. A search result or + a code-mode catalog reaches the model as ordinary tool output, and a + call-tool proxy invokes on a name the model supplies; nothing downstream + can filter either. Those surfaces have to apply the declaration here. + + A component with no ``visibility`` is visible: the field marks the + exception, and the spec's default is both audiences. + """ + meta = component.meta + if not meta: + return True + ui_meta = meta.get("ui") + if not isinstance(ui_meta, dict): + return True + visibility = ui_meta.get("visibility") + if not isinstance(visibility, list): + return True + return "model" in visibility diff --git a/fastmcp_slim/fastmcp/server/transforms/catalog.py b/fastmcp_slim/fastmcp/server/transforms/catalog.py index 936fcd9b3..e1ea30486 100644 --- a/fastmcp_slim/fastmcp/server/transforms/catalog.py +++ b/fastmcp_slim/fastmcp/server/transforms/catalog.py @@ -49,6 +49,7 @@ from collections.abc import Sequence from contextvars import ContextVar from typing import TYPE_CHECKING +from fastmcp.apps.config import is_model_visible from fastmcp.server.transforms import Transform from fastmcp.utilities.versions import dedupe_with_versions @@ -177,6 +178,16 @@ class CatalogTransform(Transform): of each tool is returned — matching what protocol handlers expose on the wire. + Tools the model may not see are excluded. A catalog is read by the + model as tool output rather than advertised as ``tools/list``, so the + host filtering the spec relies on never applies to it — this is the + only place the declaration can be enforced. + + Visibility is checked after deduplication, on the version a bare name + actually reaches. Checking first would let a model-visible older + version advertise a name whose highest version is app-only, and the + call would run the version nobody was shown. + Args: ctx: The current request context. run_middleware: Whether to run middleware on the inner call. @@ -188,7 +199,8 @@ class CatalogTransform(Transform): tools = await ctx.fastmcp.list_tools(run_middleware=run_middleware) finally: self._bypass.reset(token) - return dedupe_with_versions(tools, lambda t: t.name) + selected = dedupe_with_versions(tools, lambda t: t.name) + return [tool for tool in selected if is_model_visible(tool)] async def get_resource_catalog( self, ctx: Context, *, run_middleware: bool = True diff --git a/fastmcp_slim/fastmcp/server/transforms/search/base.py b/fastmcp_slim/fastmcp/server/transforms/search/base.py index cfeac6909..cdb47680b 100644 --- a/fastmcp_slim/fastmcp/server/transforms/search/base.py +++ b/fastmcp_slim/fastmcp/server/transforms/search/base.py @@ -31,6 +31,7 @@ from abc import abstractmethod from collections.abc import Awaitable, Callable, Sequence from typing import Annotated, Any +from fastmcp.exceptions import NotFoundError from fastmcp.server.context import Context from fastmcp.server.transforms import GetToolNext from fastmcp.server.transforms.catalog import CatalogTransform @@ -240,6 +241,13 @@ class BaseSearchTransform(CatalogTransform): raise ValueError( f"'{name}' is a synthetic search tool and cannot be called via the call_tool proxy" ) + # The name comes from the model, so this proxy is a second way + # into the server that no host mediates. It may reach only what + # the model was allowed to discover. + if not any( + tool.name == name for tool in await transform.get_tool_catalog(ctx) + ): + raise NotFoundError(f"Unknown tool: {name!r}") return await ctx.fastmcp.call_tool(name, arguments) return Tool.from_function(fn=call_tool, name=self._call_tool_name) diff --git a/tests/server/transforms/test_model_visibility_boundary.py b/tests/server/transforms/test_model_visibility_boundary.py new file mode 100644 index 000000000..f1d98b67d --- /dev/null +++ b/tests/server/transforms/test_model_visibility_boundary.py @@ -0,0 +1,168 @@ +"""App-only tools must not reach the model through server-driven surfaces. + +`tools/list` carries app-only tools on purpose — intermediaries need them to +forward, and the MCP Apps spec puts visibility filtering on the host. That +division holds only where a host sits between the server and the model. + +A search result, a code-mode catalog, and a call-tool proxy are all driven by +the server itself: the first two reach the model as ordinary tool output, and +the third invokes on a name the model supplies. No host mediates any of them, +so the visibility declaration has to be applied server-side. +""" + +from __future__ import annotations + +import json + +import pytest + +from fastmcp import Client, FastMCP, FastMCPApp +from fastmcp.exceptions import ToolError +from fastmcp.experimental.transforms.code_mode import CodeMode +from fastmcp.server.providers.addressing import hashed_backend_name +from fastmcp.server.transforms.search import BM25SearchTransform, RegexSearchTransform +from fastmcp.tools.base import Tool + + +def build_server_without_transform() -> FastMCP: + return _build(None) + + +def build_server(transform) -> FastMCP: + return _build(transform) + + +def _build(transform) -> FastMCP: + app = FastMCPApp("contacts") + + @app.tool() + def save_contact(name: str) -> str: + """UI-only backend that writes a contact.""" + return f"saved {name}" + + @app.tool(model=True) + def search_contacts(query: str) -> str: + """Model-visible backend.""" + return f"found {query}" + + @app.ui() + def contacts_ui() -> str: + return "ui" + + server = FastMCP("Platform") + server.add_provider(app) + if transform is not None: + server.add_transform(transform) + return server + + +CATALOG_TRANSFORMS = [ + pytest.param(RegexSearchTransform, id="regex-search"), + pytest.param(BM25SearchTransform, id="bm25-search"), + pytest.param(CodeMode, id="code-mode"), +] + + +@pytest.mark.parametrize("transform_cls", CATALOG_TRANSFORMS) +async def test_app_only_tools_stay_out_of_model_catalogs(transform_cls): + """Discovery surfaces hand tool definitions straight to the model.""" + server = build_server(transform_cls()) + + async with Client(server) as client: + blob = "" + for tool in await client.list_tools(): + if "search" not in tool.name: + continue + # Each transform names its search argument differently; the + # schema is the authority. + (argument,) = (tool.input_schema or {}).get("required", ["query"]) + result = await client.call_tool(tool.name, {argument: "search_contacts"}) + blob += json.dumps(result.structured_content or "") + blob += "".join( + block.text for block in result.content if hasattr(block, "text") + ) + + assert blob, "no search surface produced output" + assert "save_contact" not in blob + assert "search_contacts" in blob + + +async def test_app_only_tools_are_listed_for_forwarding(): + """The wire listing keeps them: a proxy cannot forward what it cannot see. + + Only the model-facing catalog is filtered, so a server without a catalog + transform still advertises the tool and its declaration for a host to + act on. + """ + plain = build_server_without_transform() + + async with Client(plain) as client: + listed = {tool.name: tool for tool in await client.list_tools()} + + assert "save_contact" in listed + assert listed["save_contact"].meta is not None + assert listed["save_contact"].meta["ui"]["visibility"] == ["app"] + + +async def test_call_tool_proxy_refuses_undiscoverable_tools(): + """The proxy takes a model-supplied name, so it is a second door in.""" + server = build_server(RegexSearchTransform()) + + async with Client(server) as client: + with pytest.raises(ToolError, match="save_contact"): + await client.call_tool( + "call_tool", + {"name": "save_contact", "arguments": {"name": "eve"}}, + ) + + allowed = await client.call_tool( + "call_tool", + {"name": "search_contacts", "arguments": {"query": "ada"}}, + ) + assert allowed.content[0].text == "found ada" # type: ignore[union-attr] + + +@pytest.mark.parametrize("transform_cls", CATALOG_TRANSFORMS) +async def test_the_apps_own_ui_still_reaches_its_backend(transform_cls): + """The point of the boundary is the audience, not the tool: a UI calling + by identity is not the model, and must still work. + """ + server = build_server(transform_cls()) + + result = await server.call_tool( + hashed_backend_name("contacts", "save_contact"), {"name": "ada"} + ) + assert result.content[0].text == "saved ada" # type: ignore[union-attr] # ty:ignore[unresolved-attribute] + + +async def test_visibility_is_checked_on_the_version_a_name_reaches(): + """A bare name selects the highest version, so that is the one whose + declaration governs. Checking before deduplication would advertise a + model-visible older version whose name runs an app-only newer one. + """ + + def versioned(version: str, visibility: list[str], marker: str) -> Tool: + def same() -> str: + return f"ran {marker}" + + return Tool.from_function( + same, name="same", version=version, meta={"ui": {"visibility": visibility}} + ) + + app = FastMCPApp("contacts") + app.add_tool(versioned("1.0.0", ["app", "model"], "v1")) + app.add_tool(versioned("2.0.0", ["app"], "v2")) + + server = FastMCP("Platform") + server.add_provider(app) + server.add_transform(RegexSearchTransform()) + + async with Client(server) as client: + found = await client.call_tool("search_tools", {"pattern": "same"}) + blob = json.dumps(found.structured_content or "") + "".join( + block.text for block in found.content if hasattr(block, "text") + ) + assert "same" not in blob + + with pytest.raises(ToolError, match="same"): + await client.call_tool("call_tool", {"name": "same", "arguments": {}}) From 8b76710e66582d640da53606518ebc6437a51ec9 Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Tue, 28 Jul 2026 16:31:49 -0400 Subject: [PATCH 14/53] Move to the stable MCP Python SDK 2.0.0 (#4655) --- dev-docs/v4-notes/change-register.md | 21 +- docs/apps/low-level.mdx | 2 +- docs/clients/notifications.mdx | 6 +- docs/clients/sampling.mdx | 4 +- .../upgrading/from-fastmcp-3.mdx | 37 +- .../upgrading/from-low-level-sdk-v1.mdx | 5 +- .../upgrading/from-mcp-sdk-v1.mdx | 8 +- .../upgrading/from-mcp-sdk-v2.mdx | 3 + docs/getting-started/whats-new.mdx | 4 +- docs/integrations/chatgpt.mdx | 2 +- docs/more/faq.mdx | 2 +- docs/servers/completions.mdx | 10 +- docs/servers/context.mdx | 2 +- docs/servers/elicitation.mdx | 4 +- docs/servers/icons.mdx | 14 +- docs/servers/sampling.mdx | 2 +- docs/servers/tasks.mdx | 2 +- docs/servers/tools.mdx | 6 +- .../fastmcp/client/auth/client_credentials.py | 4 +- fastmcp_slim/fastmcp/client/auth/oauth.py | 1 - fastmcp_slim/fastmcp/client/client.py | 45 +- fastmcp_slim/fastmcp/client/messages.py | 61 +- fastmcp_slim/fastmcp/client/progress.py | 2 +- .../fastmcp/client/transports/base.py | 1 + .../fastmcp/server/auth/oauth_proxy/proxy.py | 13 +- .../server/auth/redirect_validation.py | 6 +- fastmcp_slim/fastmcp/server/context.py | 10 +- .../fastmcp/server/providers/proxy.py | 30 +- fastmcp_slim/fastmcp/utilities/inspect.py | 4 +- fastmcp_slim/pyproject.toml | 4 +- pyproject.toml | 6 +- tests/client/client/test_client.py | 5 +- tests/client/client/test_mode_negotiation.py | 5 +- tests/conformance/test_conformance.py | 2 +- tests/conftest.py | 16 + tests/resources/test_resources.py | 5 +- .../providers/proxy/test_proxy_server.py | 15 +- tests/server/test_protocol_eras.py | 9 +- tests/test_upgrade_from_v3.py | 15 +- tests/tools/tool/test_results.py | 14 +- uv.lock | 2296 +++++++++-------- 41 files changed, 1487 insertions(+), 1216 deletions(-) diff --git a/dev-docs/v4-notes/change-register.md b/dev-docs/v4-notes/change-register.md index 89762a6ef..eb1db6549 100644 --- a/dev-docs/v4-notes/change-register.md +++ b/dev-docs/v4-notes/change-register.md @@ -6,7 +6,7 @@ This is the complete register of user-facing changes from the MCP Python SDK v2 Each entry is tagged **Absorbed** (public surface unchanged), **Bridged** (shim keeps old code working, usually warning), **Breaking** (user code must change), or **Deprecated** (works, warns, slated for removal). See the [overview](index.md) for what each disposition means. -**Empirical validation (WS2 upgrade reality-check).** The register's compatibility claims are verified, not predicted. Running unchanged 3.x-era code against this branch, all 11 upgrade scenarios pass or warn — the only failures are the two predicted breaks, user `mcp.types` imports and positional `McpError(ErrorData(...))` construction. Cross-version wire interop between a 3.4.3 peer and this branch is bidirectionally clean across 9 operations (3.4.3 client ↔ v4 server and v4 client ↔ 3.4.3 server over HTTP). All 29 `_ALIASES` bridge entries warn correctly with actionable messages. +**Empirical validation (WS2 upgrade reality-check).** The register's compatibility claims are verified, not predicted. Running unchanged 3.x-era code against this branch, all 11 upgrade scenarios pass or warn — the only failures were the two predicted breaks, user `mcp.types` imports and positional `McpError(ErrorData(...))` construction — and the first of those went away when the stable SDK restored `mcp.types` (below). Cross-version wire interop between a 3.4.3 peer and this branch is bidirectionally clean across 9 operations (3.4.3 client ↔ v4 server and v4 client ↔ 3.4.3 server over HTTP). All 29 `_ALIASES` bridge entries warn correctly with actionable messages. ## Environment @@ -18,14 +18,31 @@ The SDK v2 raises FastMCP's dependency floors. Projects pinning an older pydanti ## Types and imports -The SDK v2 split protocol types into a standalone `mcp_types` package and renamed every field from camelCase to snake_case. This is the single largest source of user-facing change, and FastMCP absorbs nearly all of it. +The SDK v2 moved protocol types into a standalone `mcp_types` package — still importable as `mcp.types` — and renamed every model field from camelCase to snake_case in Python. The wire format is unchanged: the models keep their camelCase aliases and the SDK serializes with `by_alias=True`, so this renames the attributes code reads, not the JSON on the connection. This is the single largest source of user-facing change, and FastMCP absorbs nearly all of it. ### `mcp.types` split into `mcp_types` — Breaking (by omission) + +Superseded by the stable SDK — see "`mcp.types` restored as a permanent alias" below. The betas this section was written against had no `mcp.types`; `2.0.0` brought it back, so the break never reached a release. + + The `mcp.types` module no longer exists. Any `from mcp.types import X` or `import mcp.types` in user code raises `ImportError`. This is the one import change users cannot avoid. *Verify:* `fastmcp_slim/fastmcp/types.py`, and grep the diff for the doc migration `from mcp.types import` → `from fastmcp.types import` (30 sites). +### `mcp.types` restored as a permanent alias — Absorbed (stable-SDK change) + +The SDK betas removed `mcp.types` outright, which made user imports the one unavoidable break in the migration. SDK `2.0.0` reintroduced it as a permanent alias for `mcp_types`: a wildcard mirror where every name is the *same object* (`mcp.types.Tool is mcp_types.Tool`), with matching `__all__` and the same snake_case fields. It is not a v1 restoration — only the import path came back. So `from mcp.types import X` keeps working, and the break is gone. + +This leaves the two spellings pointing at one package, and FastMCP uses each in a different place on purpose: + +- **User-facing docs and examples use `mcp.types`.** Anyone installing `fastmcp` gets the full SDK (`fastmcp` → `fastmcp-slim[client,server]` → `[mcp]` → `mcp`), so the aliased path always resolves and is the spelling the SDK prefers. It also means a user's own dependency list needs only `mcp`, without naming `mcp-types` to satisfy a linter. +- **FastMCP's own source uses `mcp_types`.** `mcp.types` is a submodule of `mcp`, so importing it requires the whole SDK. `mcp-types` is a *core* `fastmcp-slim` dependency while `mcp` sits behind the `[mcp]` extra, and a bare `fastmcp-slim` install must import without the SDK present — a guarantee `test_bare_slim_import_needs_only_mcp_types` pins. Reaching for `mcp.types` in core modules (`exceptions.py`, `_compat.py`, `tools/`, `resources/`) would pull the full SDK into the slim floor and break it. + +The rule of thumb: import `mcp_types` in library code, write `mcp.types` in anything a user copies. Both resolve to the same objects, so neither choice constrains the other. + +*Verify:* `.venv/.../mcp/types/__init__.py` (the wildcard mirror), `fastmcp_slim/pyproject.toml` (`mcp-types` core vs `mcp` in the `[mcp]` extra), `tests/client/test_slim_package_boundaries.py::test_bare_slim_import_needs_only_mcp_types`, and `tests/test_upgrade_from_v3.py::TestRemovedSurfacesFailLoudly::test_mcp_types_import_path_restored_by_stable_sdk`. + ### `fastmcp.types` is the stable home — Bridged diff --git a/docs/apps/low-level.mdx b/docs/apps/low-level.mdx index 46de3e03d..0cd1b0ea1 100644 --- a/docs/apps/low-level.mdx +++ b/docs/apps/low-level.mdx @@ -220,7 +220,7 @@ import qrcode from fastmcp import FastMCP from fastmcp.apps import AppConfig, ResourceCSP from fastmcp.tools import ToolResult -from mcp_types import ImageContent +from mcp.types import ImageContent mcp = FastMCP("QR Code Server") diff --git a/docs/clients/notifications.mdx b/docs/clients/notifications.mdx index b9e8dc384..1864dd23b 100644 --- a/docs/clients/notifications.mdx +++ b/docs/clients/notifications.mdx @@ -47,7 +47,7 @@ For fine-grained targeting, subclass `MessageHandler` to use specific hooks: ```python from fastmcp import Client from fastmcp.client.messages import MessageHandler -import mcp_types +import mcp.types as mcp_types class MyMessageHandler(MessageHandler): async def on_tool_list_changed( @@ -78,7 +78,7 @@ client = Client( ```python from fastmcp.client.messages import MessageHandler -import mcp_types +import mcp.types as mcp_types class MyMessageHandler(MessageHandler): async def on_message(self, message) -> None: @@ -141,7 +141,7 @@ A practical example of maintaining a tool cache that refreshes when tools change ```python from fastmcp import Client from fastmcp.client.messages import MessageHandler -import mcp_types +import mcp.types as mcp_types class ToolCacheHandler(MessageHandler): def __init__(self): diff --git a/docs/clients/sampling.mdx b/docs/clients/sampling.mdx index 746dc7ba4..f2dfe82e3 100644 --- a/docs/clients/sampling.mdx +++ b/docs/clients/sampling.mdx @@ -20,7 +20,7 @@ The handler receives the conversation the server wants completed, the parameters ```python from fastmcp import Client from fastmcp.client.sampling import SamplingMessage, SamplingParams, RequestContext -from mcp_types import TextContent +from mcp.types import TextContent async def sampling_handler( @@ -173,7 +173,7 @@ Registering any `sampling_handler` advertises full sampling support, tools inclu ```python from fastmcp import Client -from mcp_types import SamplingCapability +from mcp.types import SamplingCapability async def text_only_handler(messages, params, context) -> str: diff --git a/docs/getting-started/upgrading/from-fastmcp-3.mdx b/docs/getting-started/upgrading/from-fastmcp-3.mdx index 912f5b37b..859c1d911 100644 --- a/docs/getting-started/upgrading/from-fastmcp-3.mdx +++ b/docs/getting-started/upgrading/from-fastmcp-3.mdx @@ -5,7 +5,7 @@ description: What changes when you upgrade to FastMCP 4, which builds on the MCP icon: up --- -FastMCP 4 builds on the MCP Python SDK v2, and that is the source of every change in this guide. The SDK v2 makes two sweeping changes to the protocol layer: it splits the protocol types out of `mcp.types` into a standalone `mcp_types` package, and it renames every protocol field from camelCase to snake_case (`inputSchema` → `input_schema`, `mimeType` → `mime_type`, `isError` → `is_error`, and so on). +FastMCP 4 builds on the MCP Python SDK v2, and that is the source of every change in this guide. The SDK v2 makes two sweeping changes to the protocol layer: it moves the protocol types into a standalone `mcp_types` package (still importable as `mcp.types`), and it renames every protocol field from camelCase to snake_case (`inputSchema` → `input_schema`, `mimeType` → `mime_type`, `isError` → `is_error`, and so on). FastMCP 4 absorbs almost all of this for you. Field access is bridged so your existing reads keep working, and the imports you were taught have a stable home in FastMCP itself. What the SDK cannot hide is the protocol's own direction: the new sessionless era removes the server's ability to call back into a client mid-request, and background tasks moved out of the core spec into an extension. Those two shape the changes a working server is most likely to feel. @@ -13,21 +13,23 @@ The sections below cover what FastMCP handles for you, the changes you must make ## Install the v4 Prerelease -While FastMCP 4 is in prerelease, pin the beta and its prerelease protocol dependencies explicitly. For a uv project, add the following to `pyproject.toml`: +While FastMCP 4 is in prerelease, pin the beta explicitly. The `fastmcp` package is a thin wrapper that depends on `fastmcp-slim` at the same version, so asking for a prerelease of one means asking for a prerelease of the other. pip infers that on its own: + +```bash +pip install "fastmcp==4.0.0b1" +``` + +uv is stricter: it allows prereleases only for packages you name, and `fastmcp-slim` arrives transitively. Constrain it alongside the requirement in `pyproject.toml`: ```toml [project] dependencies = ["fastmcp==4.0.0b1"] [tool.uv] -constraint-dependencies = [ - "fastmcp-slim==4.0.0b1", - "mcp==2.0.0b2", - "mcp-types==2.0.0b2", -] +constraint-dependencies = ["fastmcp-slim==4.0.0b1"] ``` -Then run `uv lock` or `uv sync` normally. The constraints opt only these transitive packages into their prerelease versions; you do not need `--prerelease allow`, which permits prereleases throughout the dependency graph. +Then run `uv lock` or `uv sync` normally. Naming the one package keeps the rest of your graph on stable releases, where `--prerelease allow` would opt every dependency into prereleases. The MCP SDK needs no constraint at all now that it ships stable releases — pinning `mcp==2.0.0b2` here would in fact break the resolution, since a prerelease does not satisfy FastMCP's own `mcp>=2.0.0` requirement. You are upgrading an MCP server or client from FastMCP 3.x to FastMCP 4, which is built on the MCP Python SDK v2. @@ -41,7 +43,6 @@ ENVIRONMENT - a FastAPI pin below 0.133.0, the first release admitting Starlette 1.x (earlier ones cap it, e.g. 0.115.12 requires `starlette<0.47.0`), or any direct Starlette pin below 1.0.1 IMPORTS THAT NO LONGER RESOLVE -- `mcp.types` (anywhere, in any form) - `fastmcp.server.proxy`, `fastmcp.server.openapi`, `FastMCPOpenAPI` - `fastmcp.experimental.server.openapi`, `fastmcp.experimental.utilities.openapi` - `fastmcp.experimental.sampling.handlers` @@ -131,12 +132,14 @@ See [Settings](/more/settings) for the full reference. ### Protocol Types -The `mcp.types` module no longer exists. Every protocol type — `TextContent`, `ImageContent`, `Tool`, `ErrorData`, `Icon`, `PromptMessage`, `SamplingMessage`, `ToolAnnotations`, notification and request wrapper types like `ToolListChangedNotification`, and everything else — now lives in the standalone `mcp_types` package. Update your imports to point there: +Every protocol type — `TextContent`, `ImageContent`, `Tool`, `ErrorData`, `Icon`, `PromptMessage`, `SamplingMessage`, `ToolAnnotations`, notification and request wrapper types like `ToolListChangedNotification`, and everything else — now lives in a standalone `mcp_types` package. The SDK re-exports that package as `mcp.types`, so existing imports keep working and stay the preferred spelling: ```python -from mcp_types import TextContent, Tool, ToolAnnotations +from mcp.types import TextContent, Tool, ToolAnnotations ``` +Both names resolve to the same objects, so `from mcp_types import X` is equally valid — useful if you depend on the types without the rest of the SDK. What did change is the fields on those types: they are snake_case now (`input_schema`, not `inputSchema`), which the [compatibility bridge](#legacy-camelcase-field-access-keeps-working) covers for the objects FastMCP hands you. + `fastmcp.types` still exists, but holds only types FastMCP defines itself (currently just `Textarea`, used to render a multiline textarea in form-based UIs) — it does not re-export protocol types. ### The `McpError` Alias @@ -162,15 +165,7 @@ A few client behaviors that touch the SDK are preserved so you don't have to cha ## What You Must Change -Everything above, FastMCP handled for you. What remains lives in your own code, where FastMCP can't reach it — your imports, how you construct errors, the custom HTTP clients you hand to a transport, and any place you reach past FastMCP's surfaces into the raw SDK objects. Each surfaces as a clear failure at import or call time, and each is a mechanical fix. - -**Your own `mcp.types` imports.** FastMCP can re-export types, but it can't rewrite imports in your code. Any `from mcp.types import X` or `import mcp.types` in your server or client fails at import time with: - -``` -ModuleNotFoundError: No module named 'mcp.types' -``` - -The raw message gives no hint toward the fix, so if you see it after upgrading, this is why. Switch to `from mcp_types import X`. +Everything above, FastMCP handled for you. What remains lives in your own code, where FastMCP can't reach it — how you construct errors, the custom HTTP clients you hand to a transport, and any place you reach past FastMCP's surfaces into the raw SDK objects. Each surfaces as a clear failure at import or call time, and each is a mechanical fix. **`McpError` construction.** The v1 pattern of wrapping an `ErrorData` and passing it positionally fails under SDK v2 with: @@ -445,7 +440,7 @@ The client side is unaffected. `sampling_handler=` and `roots=` mean what they a Most servers upgrade untouched. Work down this list to find the ones that don't: 1. **Bump your environment.** Raise any pin below `pydantic>=2.12`; upgrade FastAPI if your resolver complains about Starlette `<1.0.1`. -2. **Fix imports that moved out.** Replace `from mcp.types import X` with `from mcp_types import X`, and update any import from the [removed modules](#moved-imports) (`fastmcp.server.proxy`, `fastmcp.server.openapi`, `fastmcp.server.apps`, the `fastmcp.tools.tool` / `resources.resource` / `prompts.prompt` component shims). +2. **Fix imports that moved out.** `from mcp.types import X` still works, but update any import from the [removed modules](#moved-imports) (`fastmcp.server.proxy`, `fastmcp.server.openapi`, `fastmcp.server.apps`, the `fastmcp.tools.tool` / `resources.resource` / `prompts.prompt` component shims). 3. **Update removed server APIs.** Swap `as_proxy` → `create_proxy`, `import_server` → `mount`, `mount(prefix=)` → `mount(namespace=)`, and the [other removed methods and keywords](#removed-server-methods). 4. **Replace `ctx.sample` and `ctx.list_roots`.** Both are gone from `Context`, as are `FastMCP(sampling_handler=...)` and `sampling_handler_behavior=`. Call an LLM directly from your server for generation; ask for roots through the guard pattern, or take file paths as tool arguments. A server whose purpose is to use the caller's model should stay on FastMCP 3.x rather than migrate. 5. **Find every `ctx.elicit()` call.** It raises on modern connections, which is what a default client now negotiates. Rewrite the tool as a guard tool, branch on `ctx.request_context.protocol_version`, or keep its clients on `mode="legacy"` — see [the era gate](#behavior-changes). diff --git a/docs/getting-started/upgrading/from-low-level-sdk-v1.mdx b/docs/getting-started/upgrading/from-low-level-sdk-v1.mdx index b7135729a..35f5412bb 100644 --- a/docs/getting-started/upgrading/from-low-level-sdk-v1.mdx +++ b/docs/getting-started/upgrading/from-low-level-sdk-v1.mdx @@ -14,7 +14,6 @@ The core idea: instead of telling the SDK what your tools look like and then sep MCP SDK v2 is a substantial, deliberate modernization of the protocol layer. Protocol types moved into a standalone `mcp_types` package, wire fields moved from camelCase to snake_case, and the low-level `Server` was rebuilt so handlers are passed to the constructor as `on_*` callables taking `(ctx, params)` rather than registered with decorators. A v1 server meets that change the moment its environment resolves `mcp` to v2: ``` -ModuleNotFoundError: No module named 'mcp.types' AttributeError: 'Server' object has no attribute 'list_tools' ``` @@ -66,7 +65,7 @@ TYPES THAT DISAPPEAR FROM YOUR CODE - `types.TextContent` wrappers around return values — return plain Python values instead - `types.ImageContent`, `types.EmbeddedResource` - `types.PromptMessage`, `types.GetPromptResult` -- Note that `mcp.types` no longer exists at all in the SDK v2 that FastMCP 4 builds on; any type that genuinely survives the rewrite comes from `mcp_types` now. +- Note that in the SDK v2 that FastMCP 4 builds on, `mcp.types` aliases the standalone `mcp_types` package; the import path still works, but the fields are snake_case now. CONTEXT AND SIDE CHANNELS - `server.request_context` @@ -92,7 +91,7 @@ uv add "fastmcp==4.0.0b1" An exact version pin installs even though it's a prerelease — neither installer needs `--pre` or `--prerelease allow` for a version this specific, only for an open-ended range. For a reproducible lockfile that also pins the prerelease protocol dependencies, see [Install the v4 Prerelease](/getting-started/upgrading/from-fastmcp-3#install-the-v4-prerelease). -FastMCP depends on the `mcp` package, so the SDK stays installed. Note that FastMCP 4 builds on SDK v2, where `mcp.types` no longer exists — protocol types live in the standalone `mcp_types` package now. Most of your `mcp.types` imports disappear entirely in the rewrite below, since FastMCP derives the protocol types from your function signatures. For the few you still need, import them from `mcp_types`. +FastMCP depends on the `mcp` package, so the SDK stays installed. FastMCP 4 builds on SDK v2, where the protocol types live in a standalone `mcp_types` package that stays importable as `mcp.types`. Most of your `mcp.types` imports disappear entirely in the rewrite below, since FastMCP derives the protocol types from your function signatures. ## Server and Transport diff --git a/docs/getting-started/upgrading/from-mcp-sdk-v1.mdx b/docs/getting-started/upgrading/from-mcp-sdk-v1.mdx index 3e08cd100..ec5ac5b7e 100644 --- a/docs/getting-started/upgrading/from-mcp-sdk-v1.mdx +++ b/docs/getting-started/upgrading/from-mcp-sdk-v1.mdx @@ -61,7 +61,7 @@ uv add "fastmcp==4.0.0b1" An exact version pin installs even though it's a prerelease — neither installer needs `--pre` or `--prerelease allow` for a version this specific, only for an open-ended range. For a reproducible lockfile that also pins the prerelease protocol dependencies, see [Install the v4 Prerelease](/getting-started/upgrading/from-fastmcp-3#install-the-v4-prerelease). -FastMCP depends on the `mcp` package, so the SDK stays installed and importable. What changes is which parts of it you reach for. FastMCP 4 builds on SDK v2, where `mcp.server.fastmcp` and `mcp.types` are both gone — anything you imported from those two modules needs a new home, and the sections below cover both. Update your import, run your server, and if your tools work, you're done. +FastMCP depends on the `mcp` package, so the SDK stays installed and importable. What changes is which parts of it you reach for. FastMCP 4 builds on SDK v2, where `mcp.server.fastmcp` is gone — anything you imported from it needs a new home, and the sections below cover that. `mcp.types` still resolves (it aliases the standalone `mcp_types` package), though its fields are snake_case now. Update your import, run your server, and if your tools work, you're done. You are upgrading an MCP server from FastMCP 1.0 (bundled in v1 of the `mcp` package) to standalone FastMCP 4. @@ -98,9 +98,9 @@ PROMPT RETURN VALUES - prompt functions returning raw dicts with "role"/"content" keys — FastMCP 1.0 coerced these silently, standalone FastMCP does not OTHER mcp.* IMPORTS -- anything from `mcp.types` — the module does not exist in the SDK v2 that FastMCP 4 builds on; protocol types moved to `mcp_types` with camelCase fields renamed to snake_case +- anything from `mcp.types` — the import path still works in the SDK v2 that FastMCP 4 builds on, but the fields were renamed from camelCase to snake_case - `from mcp.server.stdio import stdio_server` and any transport boilerplate around it -- `mcp.types.TextContent` / `ImageContent` used to wrap tool return values — FastMCP has friendlier equivalents, so prefer those over a mechanical `mcp_types` swap +- `mcp.types.TextContent` / `ImageContent` used to wrap tool return values — FastMCP has friendlier equivalents, so prefer those over keeping the raw protocol types DECORATOR RETURN VALUES - any code reading `.name`, `.description`, or other component attributes off a `@mcp.tool` / `@mcp.resource` / `@mcp.prompt` decorated function. Decorators return the original function now. @@ -215,7 +215,7 @@ def debug(error: str) -> list[Message]: ### Other `mcp.*` Imports -FastMCP 4 builds on MCP SDK v2, where the `mcp.types` module no longer exists. Protocol types moved to a standalone `mcp_types` package, and the field names were renamed from camelCase to snake_case (`inputSchema` → `input_schema`, `mimeType` → `mime_type`, and so on). Update `from mcp.types import X` to `from mcp_types import X`. For everything else SDK v2 changed, see [Upgrading from FastMCP 3](/getting-started/upgrading/from-fastmcp-3), which covers the same protocol rebuild from the FastMCP side. +FastMCP 4 builds on MCP SDK v2, which moved the protocol types into a standalone `mcp_types` package and re-exports it as `mcp.types` — so `from mcp.types import X` keeps working. The field names did change, from camelCase to snake_case (`inputSchema` → `input_schema`, `mimeType` → `mime_type`, and so on). For everything else SDK v2 changed, see [Upgrading from FastMCP 3](/getting-started/upgrading/from-fastmcp-3), which covers the same protocol rebuild from the FastMCP side. Where FastMCP provides its own API for the same thing, it's worth switching over rather than importing the protocol type: diff --git a/docs/getting-started/upgrading/from-mcp-sdk-v2.mdx b/docs/getting-started/upgrading/from-mcp-sdk-v2.mdx index 6ca0578aa..e25489005 100644 --- a/docs/getting-started/upgrading/from-mcp-sdk-v2.mdx +++ b/docs/getting-started/upgrading/from-mcp-sdk-v2.mdx @@ -228,6 +228,9 @@ if __name__ == "__main__": | `token_verifier=`, `auth_server_provider=`, `auth=AuthSettings(...)` | a single `auth=` provider | | `cache_hints={...}` | `cache_ttl=`, `cache_scope=` | | `extensions=[...]` | `mcp.add_extension(...)` | +| `middleware=[ServerMiddleware, ...]` | `middleware=[Middleware, ...]` — same keyword, different class | + +`middleware=` is the row most likely to be mistaken for a rename. Both constructors take a `middleware=` sequence, but an `MCPServer` wants the SDK's `ServerMiddleware` — one hook wrapping every raw JSON-RPC message — while FastMCP wants its own `Middleware`, which adds typed per-operation hooks (`on_call_tool`, `on_list_tools`, and the rest) on top of the same message-level pass. Keeping the keyword and swapping the base class is the migration; see [Middleware](/servers/middleware). Authentication is the largest of these, and it consolidates rather than moves. `MCPServer` exposes the SDK's raw auth plumbing — a token verifier, an authorization-server provider, and an `AuthSettings` object, configured separately. FastMCP takes one `auth=` provider that carries the whole configuration, and ships providers for the common cases: `JWTVerifier` for validating tokens you already issue, `RemoteAuthProvider` for delegating to an external authorization server, `OAuthProxy` for wrapping a provider that lacks Dynamic Client Registration, and named providers for GitHub, Google, Auth0, Keycloak, WorkOS, and others. See [Authentication](/servers/auth/authentication). diff --git a/docs/getting-started/whats-new.mdx b/docs/getting-started/whats-new.mdx index 1bb4ce45f..b2dfffb8d 100644 --- a/docs/getting-started/whats-new.mdx +++ b/docs/getting-started/whats-new.mdx @@ -13,7 +13,7 @@ FastMCP 4 is in **beta**. Pin an exact version and expect sharp edges. See [Inst ## Built on the MCP Python SDK v2 -The defining change in FastMCP 4 is the one you mostly can't see. The MCP Python SDK v2 rewrote the protocol layer end to end: it split the protocol types into a standalone `mcp_types` package, renamed every wire field from camelCase to snake_case, replaced the server's request-handling model, and made server-side middleware and multi-era serving first-class. FastMCP absorbs nearly all of it — your reads stay working through a compatibility bridge, and the handful of changes left in your code are mechanical. +The defining change in FastMCP 4 is the one you mostly can't see. The MCP Python SDK v2 rewrote the protocol layer end to end: it moved the protocol types into a standalone `mcp_types` package that stays importable as `mcp.types`, renamed every model field from camelCase to snake_case in Python, replaced the server's request-handling model, and made server-side middleware and multi-era serving first-class. FastMCP absorbs nearly all of it — your reads stay working through a compatibility bridge, and the handful of changes left in your code are mechanical. The major version is the signal. Even where your surface is unchanged, the behavior underneath is substantially different, and bumping to 4.0 is how we tell you that plainly rather than slipping a new engine in under a patch release. @@ -49,7 +49,7 @@ When a client offers autocomplete for a prompt argument or a resource-template p ```python from fastmcp import FastMCP -from mcp_types import PromptReference +from mcp.types import PromptReference mcp = FastMCP("Docs") diff --git a/docs/integrations/chatgpt.mdx b/docs/integrations/chatgpt.mdx index e999dd5af..23249f92c 100644 --- a/docs/integrations/chatgpt.mdx +++ b/docs/integrations/chatgpt.mdx @@ -95,7 +95,7 @@ The connector must be explicitly enabled in each chat session through Developer Use `annotations=ToolAnnotations(readOnlyHint=True)` to skip confirmation prompts for read-only tools: ```python -from mcp_types import ToolAnnotations +from mcp.types import ToolAnnotations @mcp.tool(annotations=ToolAnnotations(readOnlyHint=True)) def get_status() -> str: diff --git a/docs/more/faq.mdx b/docs/more/faq.mdx index fe0808580..29b0f7e89 100644 --- a/docs/more/faq.mdx +++ b/docs/more/faq.mdx @@ -8,7 +8,7 @@ icon: circle-question Most servers run untouched. The defining change in FastMCP 4 is its engine — the MCP Python SDK v2 — and FastMCP absorbs nearly all of it for you, including the wire-wide rename from camelCase to snake_case, which is bridged so your existing reads keep working. -Most of what does reach your code fails loudly at import or call time, and the fix is mechanical: your own `from mcp.types import X` becomes `from mcp_types import X`, `McpError(ErrorData(...))` becomes `McpError(code=..., message=...)`, custom `httpx` clients handed to a transport become `httpx2`, and `ctx.sample()` and `ctx.list_roots()` are gone. +Most of what does reach your code fails loudly at import or call time, and the fix is mechanical: `McpError(ErrorData(...))` becomes `McpError(code=..., message=...)`, custom `httpx` clients handed to a transport become `httpx2`, and `ctx.sample()` and `ctx.list_roots()` are gone. One change is silent, so go looking for it: an `except httpx.ConnectError:` around a FastMCP call still imports and still type-checks, because `httpx` usually remains installed through some other dependency — but FastMCP now raises the `httpx2` exception, so the handler simply stops matching and your fallback quietly never runs. Grep for `except httpx.` and move those to `httpx2`. [Upgrading from FastMCP 3](/getting-started/upgrading/from-fastmcp-3) covers each one and ends with a checklist. diff --git a/docs/servers/completions.mdx b/docs/servers/completions.mdx index e7727b0e9..d59a18488 100644 --- a/docs/servers/completions.mdx +++ b/docs/servers/completions.mdx @@ -19,7 +19,7 @@ A server has a single completion handler, registered with the `@mcp.completion` ```python from fastmcp import FastMCP -from mcp_types import PromptReference +from mcp.types import PromptReference mcp = FastMCP("Completion Server") @@ -56,7 +56,7 @@ The same handler answers completion for resource template parameters. A `Resourc ```python from fastmcp import FastMCP -from mcp_types import ResourceTemplateReference +from mcp.types import ResourceTemplateReference mcp = FastMCP("Completion Server") @@ -84,7 +84,7 @@ Completions often depend on values the user has already entered. A repository su ```python from fastmcp import FastMCP -from mcp_types import ResourceTemplateReference +from mcp.types import ResourceTemplateReference mcp = FastMCP("Completion Server") @@ -122,7 +122,7 @@ The MCP protocol caps a single response at 100 values. When more candidates exis ```python from fastmcp import FastMCP -from mcp_types import Completion, PromptReference +from mcp.types import Completion, PromptReference mcp = FastMCP("Completion Server") @@ -158,7 +158,7 @@ A completion handler may be sync or async, and it can reach the active request t ```python from fastmcp import FastMCP from fastmcp.server.dependencies import get_context -from mcp_types import PromptReference +from mcp.types import PromptReference mcp = FastMCP("Completion Server") diff --git a/docs/servers/context.mdx b/docs/servers/context.mdx index 5a7511154..01a5372d3 100644 --- a/docs/servers/context.mdx +++ b/docs/servers/context.mdx @@ -271,7 +271,7 @@ Tools can customize which components are visible to their current session using FastMCP automatically sends list change notifications when components (such as tools, resources, or prompts) are added, removed, enabled, or disabled. In rare cases where you need to manually trigger these notifications, you can use the context's notification methods: ```python -import mcp_types +import mcp.types as mcp_types @mcp.tool async def custom_tool_management(ctx: Context) -> str: diff --git a/docs/servers/elicitation.mdx b/docs/servers/elicitation.mdx index b69c2ecf1..eacde0b60 100644 --- a/docs/servers/elicitation.mdx +++ b/docs/servers/elicitation.mdx @@ -412,7 +412,7 @@ The following tool books a flight across three rounds: it asks for a destination ```python from fastmcp import FastMCP, Context -from mcp_types import InputRequiredResult, ElicitRequest, ElicitRequestFormParams +from mcp.types import InputRequiredResult, ElicitRequest, ElicitRequestFormParams mcp = FastMCP("Booking Server") @@ -549,7 +549,7 @@ This prompt gathers the context it needs before rendering: ```python from fastmcp import FastMCP, Context -from mcp_types import InputRequiredResult, ElicitRequest, ElicitRequestFormParams +from mcp.types import InputRequiredResult, ElicitRequest, ElicitRequestFormParams mcp = FastMCP("Reporting Server") diff --git a/docs/servers/icons.mdx b/docs/servers/icons.mdx index 6e2e9381a..065c28471 100644 --- a/docs/servers/icons.mdx +++ b/docs/servers/icons.mdx @@ -15,7 +15,7 @@ Icons provide visual representations for your MCP servers and components, helpin Icons use the standard MCP Icon type from the MCP protocol specification. Each icon specifies a source URL or data URI, and optionally includes MIME type, size, and theme information. ```python -from mcp_types import Icon +from mcp.types import Icon icon = Icon( src="https://example.com/icon.png", @@ -37,7 +37,7 @@ Add icons and a website URL to your server for display in client applications. M ```python from fastmcp import FastMCP -from mcp_types import Icon +from mcp.types import Icon mcp = FastMCP( name="WeatherService", @@ -66,7 +66,7 @@ Icons can be added to individual tools, resources, resource templates, and promp ### Tool Icons ```python -from mcp_types import Icon +from mcp.types import Icon @mcp.tool( icons=[Icon(src="https://example.com/calculator-icon.png")] @@ -121,7 +121,7 @@ Supply two icons with complementary `theme` values and the client picks the one ```python from fastmcp import FastMCP -from mcp_types import Icon +from mcp.types import Icon mcp = FastMCP( name="WeatherService", @@ -135,7 +135,7 @@ mcp = FastMCP( The same field works on tools, resources, resource templates, and prompts: ```python -from mcp_types import Icon +from mcp.types import Icon @mcp.tool( icons=[ @@ -155,7 +155,7 @@ Omitting `theme` means the icon is assumed suitable for any theme. That's the ri For small icons or when you want to embed the icon directly without external dependencies, use data URIs. This approach eliminates the need for hosting and ensures the icon is always available. ```python -from mcp_types import Icon +from mcp.types import Icon from fastmcp.utilities.types import Image # SVG icon as data URI @@ -175,7 +175,7 @@ def my_tool() -> str: FastMCP provides the `Image` utility class to convert local image files into data URIs. ```python -from mcp_types import Icon +from mcp.types import Icon from fastmcp.utilities.types import Image # Generate a data URI from a local image file diff --git a/docs/servers/sampling.mdx b/docs/servers/sampling.mdx index d5a39df9e..e3448bcbe 100644 --- a/docs/servers/sampling.mdx +++ b/docs/servers/sampling.mdx @@ -53,7 +53,7 @@ A tool asks for a completion by returning an `InputRequiredResult` whose `input_ ```python from fastmcp import Context, FastMCP -from mcp_types import ( +from mcp.types import ( CreateMessageRequest, CreateMessageRequestParams, CreateMessageResult, diff --git a/docs/servers/tasks.mdx b/docs/servers/tasks.mdx index e3b26ad5d..2b07bd440 100644 --- a/docs/servers/tasks.mdx +++ b/docs/servers/tasks.mdx @@ -225,7 +225,7 @@ A tool can ask the client a question partway through — the same [guard pattern ```python from fastmcp import Context, FastMCP from fastmcp_tasks import TasksExtension -import mcp_types +import mcp.types as mcp_types mcp = FastMCP("MyServer") mcp.add_extension(TasksExtension()) diff --git a/docs/servers/tools.mdx b/docs/servers/tools.mdx index c9c4e9b01..527b16b9b 100644 --- a/docs/servers/tools.mdx +++ b/docs/servers/tools.mdx @@ -723,7 +723,7 @@ For complete control over tool responses, return a `ToolResult` object. This giv ```python from fastmcp.tools import ToolResult -from mcp_types import TextContent +from mcp.types import TextContent @mcp.tool def advanced_tool() -> ToolResult: @@ -944,7 +944,7 @@ Annotations serve several purposes in client applications: You can add annotations to a tool using the `annotations` parameter in the `@mcp.tool` decorator. FastMCP accepts either a plain dict or `ToolAnnotations`; the examples below use `ToolAnnotations` for consistency and stronger editor/type support. ```python -from mcp_types import ToolAnnotations +from mcp.types import ToolAnnotations @mcp.tool( annotations=ToolAnnotations( @@ -983,7 +983,7 @@ Mark a tool as read-only when it retrieves data, performs calculations, or check ```python from fastmcp import FastMCP -from mcp_types import ToolAnnotations +from mcp.types import ToolAnnotations mcp = FastMCP("Data Server") diff --git a/fastmcp_slim/fastmcp/client/auth/client_credentials.py b/fastmcp_slim/fastmcp/client/auth/client_credentials.py index b6e8a6588..6cdcf6f1a 100644 --- a/fastmcp_slim/fastmcp/client/auth/client_credentials.py +++ b/fastmcp_slim/fastmcp/client/auth/client_credentials.py @@ -245,7 +245,7 @@ class ClientCredentialsOAuthProvider(_SDKClientCredentialsOAuthProvider): client_id=self._client_id, client_secret=self._client_secret, token_endpoint_auth_method=self._token_endpoint_auth_method, - scopes=self._scopes, + scope=self._scopes, ) self._bound = True @@ -371,7 +371,7 @@ class PrivateKeyJWTOAuthProvider(_SDKPrivateKeyJWTOAuthProvider): ), client_id=self._client_id, assertion_provider=self._assertion_provider, - scopes=self._scopes, + scope=self._scopes, ) self._bound = True diff --git a/fastmcp_slim/fastmcp/client/auth/oauth.py b/fastmcp_slim/fastmcp/client/auth/oauth.py index a73ea14aa..442e6ad33 100644 --- a/fastmcp_slim/fastmcp/client/auth/oauth.py +++ b/fastmcp_slim/fastmcp/client/auth/oauth.py @@ -344,7 +344,6 @@ class OAuth(OAuthClientProvider): storage=self.token_storage_adapter, redirect_handler=self.redirect_handler, callback_handler=self.callback_handler, - timeout=self._callback_timeout, client_metadata_url=self._client_metadata_url, ) diff --git a/fastmcp_slim/fastmcp/client/client.py b/fastmcp_slim/fastmcp/client/client.py index 42192830a..c9b9aa1eb 100644 --- a/fastmcp_slim/fastmcp/client/client.py +++ b/fastmcp_slim/fastmcp/client/client.py @@ -232,6 +232,22 @@ class ClientSessionState: initialize_result: mcp_types.InitializeResult | None = None +def _connection_failure(exception: BaseException) -> BaseException: + """Present a dead session the same way wherever it is noticed. + + A failed session surfaces from two places: `_connect`, when the connection + never comes up, and `_await_with_session_monitoring`, when the session task + dies while a request is in flight. Which one wins is a matter of timing, so + both report the failure identically — otherwise the same dead backend + reaches callers as either a `RuntimeError` naming the connection or the raw + transport error, depending on the race. Types callers reasonably branch on + are passed through untouched. + """ + if isinstance(exception, httpx2.HTTPStatusError | MCPError): + return exception + return RuntimeError(f"Client failed to connect: {exception}") + + @dataclass class CallToolResult: """Parsed result from a tool call.""" @@ -530,6 +546,14 @@ class Client( "sampling_callback": None, "list_roots_callback": None, "logging_callback": create_log_callback(log_handler), + # Log delivery is opt-in per request on the modern protocol: the + # session stamps this level into each request's `_meta`, and a + # server sends nothing without it. FastMCP's contract is that a + # client receives everything unless it narrows the level itself, so + # request the most permissive level and let the server's own + # `client_log_level` (and legacy `set_logging_level`) do the + # filtering. Inert on the handshake eras, which have no such opt-in. + "log_level": "debug", "message_handler": effective_message_handler, "read_timeout_seconds": read_timeout_seconds, "client_info": client_info, @@ -982,18 +1006,23 @@ class Client( raise - if self._session_state.session_task.done(): - exception = self._session_state.session_task.exception() + session_task = self._session_state.session_task + if not session_task.done() and self._session_state.session is None: + # `_session_runner` sets `ready_event` from its `finally`, + # so a failed connect can wake the wait above before the + # task is marked done. No session means the connect failed, + # so let the task settle and report the failure here rather + # than letting the raw transport error escape on the next + # request. + await asyncio.wait([session_task], timeout=3) + + if session_task.done(): + exception = session_task.exception() if exception is None: raise RuntimeError( "Session task completed without exception but connection failed" ) - # Preserve specific exception types that clients may want to handle - if isinstance(exception, httpx2.HTTPStatusError | MCPError): - raise exception - raise RuntimeError( - f"Client failed to connect: {exception}" - ) from exception + raise _connection_failure(exception) from exception self._session_state.nesting_counter += 1 diff --git a/fastmcp_slim/fastmcp/client/messages.py b/fastmcp_slim/fastmcp/client/messages.py index 7183a8e67..8dfbfd964 100644 --- a/fastmcp_slim/fastmcp/client/messages.py +++ b/fastmcp_slim/fastmcp/client/messages.py @@ -2,57 +2,32 @@ from typing import TypeAlias import mcp_types from mcp.client.session import MessageHandlerFnT -from mcp.shared.session import RequestResponder -Message: TypeAlias = ( - RequestResponder[mcp_types.ServerRequest, mcp_types.ClientResult] - | mcp_types.ServerNotification - | Exception -) +Message: TypeAlias = mcp_types.ServerNotification | Exception MessageHandlerT: TypeAlias = MessageHandlerFnT class MessageHandler: """ - This class is used to handle MCP messages sent to the client. It is used to handle all messages, - requests, notifications, and exceptions. Users can override any of the hooks + This class is used to handle MCP messages sent to the client: notifications + and transport-level exceptions. Users can override any of the hooks. + + Server-initiated *requests* (ping, sampling, roots) never reach this + handler: the stable MCP SDK v2's `message_handler` contract only delivers + `ServerNotification | Exception`, so a request has no wire path here. + Those are answered through the `Client`'s dedicated callbacks instead — + `sampling_handler=`, `roots=`, and `elicitation_handler=`. """ - async def __call__( - self, - message: RequestResponder[mcp_types.ServerRequest, mcp_types.ClientResult] - | mcp_types.ServerNotification - | Exception, - ) -> None: + async def __call__(self, message: mcp_types.ServerNotification | Exception) -> None: return await self.dispatch(message) async def dispatch(self, message: Message) -> None: # handle all messages await self.on_message(message) - # SDK v2 delivers server-to-client requests wrapped in a - # RequestResponder (with the request unwrapped on `.request`) and - # notifications unwrapped (the monolith notification model itself, no - # `.root` wrapper). `ServerNotification`/`ServerRequest` are UnionTypes, - # so they can't appear in class match patterns — branch on the concrete - # models directly. - if isinstance(message, RequestResponder): - # handle all requests - # ty doesn't narrow the generic RequestResponder cleanly here. - await self.on_request(message) # type: ignore[arg-type] # ty:ignore[invalid-argument-type] - - # handle specific requests - request = message.request - match request: - case mcp_types.PingRequest(): - await self.on_ping(request) - case mcp_types.ListRootsRequest(): - await self.on_list_roots(request) - case mcp_types.CreateMessageRequest(): - await self.on_create_message(request) - - elif isinstance(message, Exception): + if isinstance(message, Exception): await self.on_exception(message) else: @@ -79,20 +54,6 @@ class MessageHandler: async def on_message(self, message: Message) -> None: pass - async def on_request( - self, message: RequestResponder[mcp_types.ServerRequest, mcp_types.ClientResult] - ) -> None: - pass - - async def on_ping(self, message: mcp_types.PingRequest) -> None: - pass - - async def on_list_roots(self, message: mcp_types.ListRootsRequest) -> None: - pass - - async def on_create_message(self, message: mcp_types.CreateMessageRequest) -> None: - pass - async def on_notification(self, message: mcp_types.ServerNotification) -> None: pass diff --git a/fastmcp_slim/fastmcp/client/progress.py b/fastmcp_slim/fastmcp/client/progress.py index 826d2cb99..72392305c 100644 --- a/fastmcp_slim/fastmcp/client/progress.py +++ b/fastmcp_slim/fastmcp/client/progress.py @@ -1,6 +1,6 @@ from typing import TypeAlias -from mcp.shared.session import ProgressFnT +from mcp.shared.dispatcher import ProgressFnT from fastmcp.utilities.logging import get_logger diff --git a/fastmcp_slim/fastmcp/client/transports/base.py b/fastmcp_slim/fastmcp/client/transports/base.py index 760e0a2e8..422f3ed9c 100644 --- a/fastmcp_slim/fastmcp/client/transports/base.py +++ b/fastmcp_slim/fastmcp/client/transports/base.py @@ -29,6 +29,7 @@ class ClientSessionKwargs(TypedDict, total=False): sampling_capabilities: mcp_types.SamplingCapability | None list_roots_callback: ListRootsFnT | None logging_callback: LoggingFnT | None + log_level: mcp_types.LoggingLevel | None elicitation_callback: ElicitationFnT | None message_handler: MessageHandlerFnT | None client_info: mcp_types.Implementation | None diff --git a/fastmcp_slim/fastmcp/server/auth/oauth_proxy/proxy.py b/fastmcp_slim/fastmcp/server/auth/oauth_proxy/proxy.py index fda590bae..422ed3b49 100644 --- a/fastmcp_slim/fastmcp/server/auth/oauth_proxy/proxy.py +++ b/fastmcp_slim/fastmcp/server/auth/oauth_proxy/proxy.py @@ -983,9 +983,18 @@ class OAuthProxy(OAuthProvider, ConsentMixin): # builds this object, so prefer the value the HTTP route recovered from # the raw request body. Fall back to the object's own field for direct # (non-HTTP) callers. Write it back so the DCR response echoes the type. + # + # The SDK splits the registration *request* model from the registered + # *client record*: `OAuthClientMetadata.application_type` defaults to + # "native", while `OAuthClientInformationFull.application_type` is + # `str | None` and defaults to None. Normalize the unset case back to + # "native" so a client that omits the field gets the RFC 7591 default + # recorded explicitly, on both the HTTP and direct-call paths. pending_application_type = _pending_application_type.get() if pending_application_type is not None: client_info.application_type = pending_application_type + elif client_info.application_type is None: + client_info.application_type = "native" application_type = client_info.application_type if client_info.redirect_uris: @@ -1161,7 +1170,7 @@ class OAuthProxy(OAuthProvider, ConsentMixin): # Store transaction data for IdP callback processing if client.client_id is None: raise AuthorizeError( - error="invalid_client", # type: ignore[arg-type] # "invalid_client" is valid OAuth error but not in Literal type # ty:ignore[invalid-argument-type] + error="invalid_client", # type: ignore[arg-type] # "invalid_client" is valid OAuth error but not in Literal type error_description="Client ID is required", ) # Clients may omit `scope` entirely, in which case OAuth lets the @@ -1254,7 +1263,7 @@ class OAuthProxy(OAuthProvider, ConsentMixin): # Create authorization code object with PKCE challenge if client.client_id is None: raise AuthorizeError( - error="invalid_client", # type: ignore[arg-type] # "invalid_client" is valid OAuth error but not in Literal type # ty:ignore[invalid-argument-type] + error="invalid_client", # type: ignore[arg-type] # "invalid_client" is valid OAuth error but not in Literal type error_description="Client ID is required", ) return AuthorizationCode( diff --git a/fastmcp_slim/fastmcp/server/auth/redirect_validation.py b/fastmcp_slim/fastmcp/server/auth/redirect_validation.py index a2519d567..9bb3c1e69 100644 --- a/fastmcp_slim/fastmcp/server/auth/redirect_validation.py +++ b/fastmcp_slim/fastmcp/server/auth/redirect_validation.py @@ -368,7 +368,7 @@ def matches_allowed_pattern(uri: str, pattern: str) -> bool: def is_redirect_uri_allowed_for_application_type( redirect_uri: str | AnyUrl, - application_type: str, + application_type: str | None, ) -> bool: """Check a redirect URI against RFC 7591 / SEP-837 `application_type` rules. @@ -399,7 +399,9 @@ def is_redirect_uri_allowed_for_application_type( The MCP SDK defaults `application_type` to `"native"` because MCP clients typically register loopback redirect URIs, so omitting the field preserves - the behavior clients relied on before this check existed. + the behavior clients relied on before this check existed. `None` — which a + registered-client record carries when the field was never set — is treated + the same way. """ uri_str = str(redirect_uri) diff --git a/fastmcp_slim/fastmcp/server/context.py b/fastmcp_slim/fastmcp/server/context.py index 4b5a88082..bd089289a 100644 --- a/fastmcp_slim/fastmcp/server/context.py +++ b/fastmcp_slim/fastmcp/server/context.py @@ -892,7 +892,15 @@ class Context: """ # v2: ServerNotification is a union of concrete notification models; # ServerSession.send_notification takes an instance directly (no wrapper). - await self.session.send_notification(notification) + # + # Relate the notification to the in-flight request so it rides that + # request's own stream. A sessionless (2026-07-28) connection has no + # standing server→client channel, so an unrelated notification is + # dropped; the request's stream is the only way out. Session-based eras + # deliver it either way. + await self.session.send_notification( + notification, related_request_id=self.request_id + ) async def close_sse_stream(self) -> None: """Close the current response stream to trigger client reconnection. diff --git a/fastmcp_slim/fastmcp/server/providers/proxy.py b/fastmcp_slim/fastmcp/server/providers/proxy.py index 0d7ef3a78..9b1cd4a69 100644 --- a/fastmcp_slim/fastmcp/server/providers/proxy.py +++ b/fastmcp_slim/fastmcp/server/providers/proxy.py @@ -31,7 +31,7 @@ from mcp_types import ( from mcp_types.version import MODERN_PROTOCOL_VERSIONS from pydantic.networks import AnyUrl -from fastmcp.client.client import Client, SDKServer +from fastmcp.client.client import Client, SDKServer, _connection_failure from fastmcp.client.elicitation import ElicitResult, create_elicitation_callback from fastmcp.client.logging import LogMessage, create_log_callback from fastmcp.client.roots import RootsList, create_roots_callback @@ -116,9 +116,17 @@ _PROXY_TRANSPORT_ERRORS: tuple[type[Exception], ...] = ( def _proxy_upstream_error(error: Exception) -> MCPError: + """Report an unreachable backend the same way however the failure arrived. + + Depending on where the dead connection is noticed, the proxy sees either + FastMCP's own `RuntimeError("Client failed to connect: ...")` or the raw + transport error underneath it. Both describe one thing — the proxy could + not reach its upstream — so both are presented identically rather than + leaking the race into the message the front client reads. + """ return MCPError( code=mcp_types.INTERNAL_ERROR, - message=str(error), + message=str(_connection_failure(error)), ) @@ -1311,10 +1319,20 @@ class FastMCPProxy(FastMCP): try: async with client: result.instructions = client.session.instructions - except MCPError: - raise - except _PROXY_TRANSPORT_ERRORS as error: - raise _proxy_upstream_error(error) from error + except (MCPError, *_PROXY_TRANSPORT_ERRORS) as error: + # Instructions are optional metadata, so an unreachable backend + # must not fail negotiation itself. Failing here would surface + # as a confusing protocol error: the client's auto-negotiation + # reads any `server/discover` error as "not a modern server" + # and retries with the initialize handshake, which this + # modern-serving proxy then rejects — hiding the real cause. + # Answer without upstream instructions instead and let the + # backend failure surface on the first real operation, where + # the proxy reports it as an upstream connection error. + logger.debug( + "Could not read upstream instructions for server/discover: %r", + error, + ) return result self._mcp_server.add_request_handler( diff --git a/fastmcp_slim/fastmcp/utilities/inspect.py b/fastmcp_slim/fastmcp/utilities/inspect.py index 1ce403643..2348f3410 100644 --- a/fastmcp_slim/fastmcp/utilities/inspect.py +++ b/fastmcp_slim/fastmcp/utilities/inspect.py @@ -391,10 +391,12 @@ async def inspect_fastmcp_v1(mcp: SDKServer) -> FastMCPInfo: # SDK v2's MCPServer (FastMCP 1.x) exposes name/instructions/version # directly; the v1 `_mcp_server` low-level wrapper attribute is gone. + # It defaults `version` to an empty string rather than None, so report + # an unset version as absent instead of blank. return FastMCPInfo( name=mcp.name, instructions=mcp.instructions, - version=mcp.version, + version=mcp.version or None, website_url=server_website_url, icons=server_icons, fastmcp_version=fastmcp.__version__, # Version generating this manifest diff --git a/fastmcp_slim/pyproject.toml b/fastmcp_slim/pyproject.toml index e23dfca8c..ba408404d 100644 --- a/fastmcp_slim/pyproject.toml +++ b/fastmcp_slim/pyproject.toml @@ -4,7 +4,7 @@ dynamic = ["version", "optional-dependencies"] description = "The dependency-slim FastMCP package." authors = [{ name = "Jeremiah Lowin" }] dependencies = [ - "mcp-types==2.0.0b2", + "mcp-types>=2.0.0,<3.0.0", "platformdirs>=4.0.0", "pydantic[email]>=2.12.0", "pydantic-settings>=2.0.0", @@ -82,7 +82,7 @@ mcp = [ # client auth) requires it, and all FastMCP-owned HTTP (server auth provider # upstream calls, OpenAPI provider, version check, etc.) uses it too. "httpx2>=2.5.0", - "mcp==2.0.0b2", + "mcp>=2.0.0,<3.0.0", "opentelemetry-api>=1.28.0", # starlette floor: transitive via mcp (which only requires >=0.27). # Pin past CVE-2026-48710, which was patched in 1.0.1. diff --git a/pyproject.toml b/pyproject.toml index 0f673a848..005194822 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -72,7 +72,11 @@ members = ["fastmcp_slim", "fastmcp_remote", "fastmcp_tasks"] [tool.uv] default-groups = ["dev"] exclude-newer = "1 week" -exclude-newer-package = { fastmcp = false, fastmcp-slim = false, fastmcp-remote = false, prefab-ui = false, mcp = false, mcp-types = false, httpx2 = false, httpcore2 = false, truststore = false } +# The cooldown above refuses anything published in the last week. Exempt the +# first-party packages, whose fresh releases we install deliberately, and the +# MCP SDK, where a new major is the only version satisfying our floor and so +# has nothing older to fall back to. +exclude-newer-package = { fastmcp = false, fastmcp-slim = false, fastmcp-remote = false, prefab-ui = false, mcp = false, mcp-types = false } [dependency-groups] dev = [ diff --git a/tests/client/client/test_client.py b/tests/client/client/test_client.py index d0edc194a..0db2a7fa9 100644 --- a/tests/client/client/test_client.py +++ b/tests/client/client/test_client.py @@ -18,6 +18,7 @@ from fastmcp.client.transports import ( FastMCPTransport, ) from fastmcp.server.server import FastMCP +from tests.conftest import user_meta async def test_list_tools(fastmcp_server): @@ -848,7 +849,7 @@ async def test_client_unwraps_result_using_meta(): result = await client.call_tool("list_tool", {}) assert result.structured_content == {"result": [1, 2, 3]} assert result.data == [1, 2, 3] - assert result.meta == {"fastmcp": {"wrap_result": True}} + assert user_meta(result.meta) == {"fastmcp": {"wrap_result": True}} async def test_client_does_not_unwrap_dict_result(): @@ -864,7 +865,7 @@ async def test_client_does_not_unwrap_dict_result(): result = await client.call_tool("dict_tool", {}) assert result.structured_content == {"a": 1} assert result.data == {"a": 1} - assert result.meta is None + assert user_meta(result.meta) is None async def test_client_list_dict_return_type(): diff --git a/tests/client/client/test_mode_negotiation.py b/tests/client/client/test_mode_negotiation.py index 66904d2ee..1d400a727 100644 --- a/tests/client/client/test_mode_negotiation.py +++ b/tests/client/client/test_mode_negotiation.py @@ -244,12 +244,11 @@ class TestNonConformantModernPeer: class TestPinnedMode: async def test_pinned_modern_adopts_without_probe(self, fastmcp_server): """Pinning the modern version adopts it directly; a synthesized - DiscoverResult leaves server_info empty.""" + DiscoverResult carries no identity, so server_info is absent.""" async with Client(fastmcp_server, mode=LATEST_MODERN_VERSION) as client: assert client.protocol_version == LATEST_MODERN_VERSION assert client.initialize_result is None - assert client.server_info is not None - assert client.server_info.name == "" + assert client.server_info is None assert client.instructions is None async def test_pinned_modern_call_tool(self, fastmcp_server): diff --git a/tests/conformance/test_conformance.py b/tests/conformance/test_conformance.py index 824c7b401..5c17e8eb6 100644 --- a/tests/conformance/test_conformance.py +++ b/tests/conformance/test_conformance.py @@ -28,7 +28,7 @@ EXPECTED_FAILURES = CONFORMANCE_DIR / "expected-failures.yml" HOST = "127.0.0.1" #: Pinned version of `@modelcontextprotocol/conformance`. Bump deliberately. -CONFORMANCE_VERSION = "0.2.0-alpha.9" +CONFORMANCE_VERSION = "0.2.0-alpha.10" def _get_free_port() -> int: diff --git a/tests/conftest.py b/tests/conftest.py index 84487e445..8e2fb96c7 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -8,6 +8,7 @@ from pathlib import Path from typing import Any import pytest +from mcp_types import SERVER_INFO_META_KEY from opentelemetry import trace from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import SimpleSpanProcessor @@ -23,6 +24,21 @@ if sys.platform == "win32": asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) +def user_meta(meta: dict[str, Any] | None) -> dict[str, Any] | None: + """Strip the SDK's `serverInfo` stamp from a result's `_meta`. + + Every 2026-era result carries `io.modelcontextprotocol/serverInfo` (spec + #3002), stamped by the SDK runner rather than by the component that + produced the result. Tests asserting on the meta a tool or resource set + itself use this to ignore the stamp, and get `None` back when the stamp was + the only entry. + """ + if meta is None: + return None + remaining = {k: v for k, v in meta.items() if k != SERVER_INFO_META_KEY} + return remaining or None + + def make_server_request_context( *, method: str = "tools/list", diff --git a/tests/resources/test_resources.py b/tests/resources/test_resources.py index ad72b6168..bd5af842c 100644 --- a/tests/resources/test_resources.py +++ b/tests/resources/test_resources.py @@ -5,6 +5,7 @@ from pydantic import AnyUrl, BaseModel from fastmcp import Client, FastMCP from fastmcp.resources import Resource, ResourceContent, ResourceResult from fastmcp.resources.function_resource import FunctionResource +from tests.conftest import user_meta class TestResourceValidation: @@ -323,7 +324,7 @@ class TestResourceMetaPropagation: async with Client(mcp) as client: result = await client.read_resource_mcp("test://with-meta") - assert result.meta == {"version": "2.0", "source": "test"} + assert user_meta(result.meta) == {"version": "2.0", "source": "test"} async def test_resource_content_meta_received_by_client(self): """Meta set on ResourceContent is received by MCP client.""" @@ -355,7 +356,7 @@ class TestResourceMetaPropagation: async with Client(mcp) as client: result = await client.read_resource_mcp("test://both-meta") - assert result.meta == {"result_key": "result_val"} + assert user_meta(result.meta) == {"result_key": "result_val"} assert result.contents[0].meta == {"item_key": "item_val"} async def test_json_native_return_preserves_component_meta(self): diff --git a/tests/server/providers/proxy/test_proxy_server.py b/tests/server/providers/proxy/test_proxy_server.py index fa1b2b824..8971b5ba0 100644 --- a/tests/server/providers/proxy/test_proxy_server.py +++ b/tests/server/providers/proxy/test_proxy_server.py @@ -35,6 +35,7 @@ from fastmcp.tools.tool_transform import ( ) from fastmcp.utilities.http import find_available_port from fastmcp.utilities.tests import run_server_async +from tests.conftest import user_meta USERS = [ {"id": "1", "name": "Alice", "active": True}, @@ -888,7 +889,12 @@ class TestPrompts: result = await client.get_prompt("welcome", {"name": "Alice"}) async with Client(proxy_server) as client: proxy_result = await client.get_prompt("welcome", {"name": "Alice"}) - assert proxy_result == result + # Each server stamps its own `serverInfo` into `_meta` (spec #3002), so + # the proxy's stamp naturally differs from the origin's. Compare the + # relayed payload. + assert proxy_result.model_copy( + update={"meta": user_meta(proxy_result.meta)} + ) == result.model_copy(update={"meta": user_meta(result.meta)}) async def test_render_prompt_calls_prompt(self, proxy_server): async with Client(proxy_server) as client: @@ -942,8 +948,11 @@ class TestPrompts: async with Client(proxy_server) as client: proxy_result = await client.get_prompt("image_prompt") - # The proxy result should match the original exactly - assert proxy_result == result + # The proxy relays the original payload; only the per-server + # `serverInfo` `_meta` stamp differs. + assert proxy_result.model_copy( + update={"meta": user_meta(proxy_result.meta)} + ) == result.model_copy(update={"meta": user_meta(result.meta)}) # Verify the image content is preserved as ImageContent, not JSON text assert isinstance(proxy_result.messages[1].content, mcp_types.ImageContent) assert proxy_result.messages[1].content.data == "iVBORw0KGgoAAAANSUhEUg==" diff --git a/tests/server/test_protocol_eras.py b/tests/server/test_protocol_eras.py index aabb54d68..2172b7156 100644 --- a/tests/server/test_protocol_eras.py +++ b/tests/server/test_protocol_eras.py @@ -172,6 +172,7 @@ async def test_legacy_uses_initialize_handshake(dual_era_server): """ async with SDKClient(_server(dual_era_server), mode="legacy") as client: assert client.protocol_version == "2025-11-25" + assert client.server_info is not None assert client.server_info.name == "dual-era" @@ -182,14 +183,16 @@ async def test_auto_negotiates_modern_via_discover(dual_era_server): async with SDKClient(_server(dual_era_server), mode="auto") as client: assert client.protocol_version == "2026-07-28" # server/discover carries identity, unlike the synthesized pin below. + assert client.server_info is not None assert client.server_info.name == "dual-era" assert client.server_capabilities is not None async def test_pinned_modern_adopts_without_probe(dual_era_server): """Pinning `mode='2026-07-28'` adopts the version directly. With no - `prior_discover`, the SDK synthesizes a minimal DiscoverResult, so - server_info is empty even though the protocol version is modern. + `prior_discover`, the SDK synthesizes a minimal DiscoverResult that carries + no identity, so server_info is absent even though the protocol version is + modern. Characterization of the SDK's synthesize-discover path (mcp.client.client `_synthesize_discover`): a pin without prior_discover trades identity for @@ -197,7 +200,7 @@ async def test_pinned_modern_adopts_without_probe(dual_era_server): """ async with SDKClient(_server(dual_era_server), mode="2026-07-28") as client: assert client.protocol_version == "2026-07-28" - assert client.server_info.name == "" + assert client.server_info is None # --------------------------------------------------------------------------- diff --git a/tests/test_upgrade_from_v3.py b/tests/test_upgrade_from_v3.py index bc5444e91..2f076d055 100644 --- a/tests/test_upgrade_from_v3.py +++ b/tests/test_upgrade_from_v3.py @@ -211,7 +211,6 @@ REMOVED_MODULES = [ "fastmcp.experimental.utilities.openapi", # -> fastmcp.utilities.openapi "fastmcp.server.apps", # -> fastmcp.apps "fastmcp.server.app", # -> fastmcp.apps / fastmcp - "mcp.types", # -> mcp_types # The pre-rename component modules. `tool.py`/`resource.py`/`prompt.py` are # now `base.py`; import the types from the package itself (`from # fastmcp.tools import Tool`) rather than naming the private module. @@ -244,6 +243,20 @@ class TestRemovedSurfacesFailLoudly: with pytest.raises(ModuleNotFoundError): importlib.import_module(module_path) + def test_mcp_types_import_path_restored_by_stable_sdk(self): + # The MCP Python SDK beta (2.0.0b2, what v4 was built against) dropped + # `mcp.types` entirely, so `from mcp.types import X` was documented as a + # hard break requiring a switch to `from mcp_types import X`. The stable + # SDK release (2.0.0) reintroduced `mcp.types` as a deliberate mirror of + # `mcp_types` — same objects, same snake_case fields, not a v1 API + # restoration — specifically so old import paths keep working. Both + # spellings resolve to the identical class. + import mcp.types + import mcp_types + + assert mcp.types.Tool is mcp_types.Tool + assert set(mcp.types.__all__) == set(mcp_types.__all__) + @pytest.mark.parametrize( "module_path, name", REMOVED_NAMES, diff --git a/tests/tools/tool/test_results.py b/tests/tools/tool/test_results.py index 39c690569..ebe26700b 100644 --- a/tests/tools/tool/test_results.py +++ b/tests/tools/tool/test_results.py @@ -8,6 +8,7 @@ from pydantic import BaseModel, ConfigDict, Field from fastmcp import Client, FastMCP from fastmcp.tools.base import Tool, ToolResult +from tests.conftest import user_meta class TestToolResultCasting: @@ -39,7 +40,7 @@ class TestToolResultCasting: assert result.content[0].type == "text" assert result.content[0].text == "test data" assert result.structured_content is None - assert result.meta is None + assert user_meta(result.meta) is None async def test_neither_unstructured_or_structured_content(self, client): from fastmcp.exceptions import ToolError @@ -56,7 +57,7 @@ class TestToolResultCasting: assert result.content[0].type == "text" assert result.content[0].text == "test data" assert result.structured_content == {"data_type": "test"} - assert result.meta is None + assert user_meta(result.meta) is None async def test_structured_unstructured_and_meta_content(self, client): result = await client.call_tool( @@ -71,7 +72,7 @@ class TestToolResultCasting: assert result.content[0].type == "text" assert result.content[0].text == "test data" assert result.structured_content == {"data_type": "test"} - assert result.meta == {"some": "metadata"} + assert user_meta(result.meta) == {"some": "metadata"} class TestToolResultIsError: @@ -153,7 +154,12 @@ class TestToolResultIsError: async with Client(mcp) as client: result = await client.call_tool_mcp("failing", {}) - assert result.model_dump(by_alias=True) == raw_result.model_dump(by_alias=True) + received = result.model_dump(by_alias=True) + # The SDK stamps `serverInfo` into every 2026-era result's `_meta` + # (spec #3002). Strip it so the assertion covers the protocol fields + # the tool itself set, which is what FastMCP is responsible for. + received["_meta"] = user_meta(received["_meta"]) + assert received == raw_result.model_dump(by_alias=True) class TestUnionReturnTypes: diff --git a/uv.lock b/uv.lock index 7bf07c4f6..a169584d7 100644 --- a/uv.lock +++ b/uv.lock @@ -4,25 +4,21 @@ requires-python = ">=3.10" resolution-markers = [ "python_full_version >= '3.14'", "python_full_version == '3.13.*'", - "python_full_version == '3.12.*'", - "python_full_version == '3.11.*'", + "python_full_version >= '3.11' and python_full_version < '3.13'", "python_full_version < '3.11'", ] [options] -exclude-newer = "2026-07-17T00:42:32.518243Z" +exclude-newer = "2026-07-21T14:39:05.08339Z" exclude-newer-span = "P1W" [options.exclude-newer-package] mcp-types = false -prefab-ui = false -truststore = false -fastmcp-slim = false fastmcp = false +prefab-ui = false mcp = false -httpcore2 = false fastmcp-remote = false -httpx2 = false +fastmcp-slim = false [manifest] members = [ @@ -36,14 +32,34 @@ members = [ name = "aiofile" version = "3.9.0" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11'", +] dependencies = [ - { name = "caio" }, + { name = "caio", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/67/e2/d7cb819de8df6b5c1968a2756c3cb4122d4fa2b8fc768b53b7c9e5edb646/aiofile-3.9.0.tar.gz", hash = "sha256:e5ad718bb148b265b6df1b3752c4d1d83024b93da9bd599df74b9d9ffcf7919b", size = 17943, upload-time = "2024-10-08T10:39:35.846Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/50/25/da1f0b4dd970e52bf5a36c204c107e11a0c6d3ed195eba0bfbc664c312b2/aiofile-3.9.0-py3-none-any.whl", hash = "sha256:ce2f6c1571538cbdfa0143b04e16b208ecb0e9cb4148e528af8a640ed51cc8aa", size = 19539, upload-time = "2024-10-08T10:39:32.955Z" }, ] +[[package]] +name = "aiofile" +version = "3.11.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version == '3.13.*'", + "python_full_version >= '3.11' and python_full_version < '3.13'", +] +dependencies = [ + { name = "caio", marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/48/41/2fea7e193e061ce54eacc3b7bc0e6a99e4fcff43c78cf0a76dd781ed8334/aiofile-3.11.1.tar.gz", hash = "sha256:1f91912c6643d2a4e49ca4ae3514f0bf3867ce948a36d99a6411b8f4755f4cf9", size = 19342, upload-time = "2026-05-16T08:18:33.538Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/67/cd/0d76dfc5de72bde52f55f53e925c7d152d9c7906634ec1e0cbc7e8d4ad93/aiofile-3.11.1-py3-none-any.whl", hash = "sha256:ce77d14ac07f77bc2b757834a5c129321f3f705c474593deed5ab209079a52c9", size = 20446, upload-time = "2026-05-16T08:18:32.051Z" }, +] + [[package]] name = "annotated-doc" version = "0.0.4" @@ -64,7 +80,7 @@ wheels = [ [[package]] name = "anthropic" -version = "0.87.0" +version = "0.117.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -76,32 +92,32 @@ dependencies = [ { name = "sniffio" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d6/8f/3281edf7c35cbac169810e5388eb9b38678c7ea9867c2d331237bd5dff08/anthropic-0.87.0.tar.gz", hash = "sha256:098fef3753cdd3c0daa86f95efb9c8d03a798d45c5170329525bb4653f6702d0", size = 588982, upload-time = "2026-03-31T17:52:41.697Z" } +sdist = { url = "https://files.pythonhosted.org/packages/41/0d/8f71d535edb0d438f023bd825fb65f67c14fa88a2bd6b75f292a58a63de4/anthropic-0.117.0.tar.gz", hash = "sha256:98107f2b76439641e0ae2a1754087534b8f178dbab99d6eb1bc4b7bc8c744496", size = 989933, upload-time = "2026-07-16T19:36:13.07Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0d/02/99bf351933bdea0545a2b6e2d812ed878899e9a95f618351dfa3d0de0e69/anthropic-0.87.0-py3-none-any.whl", hash = "sha256:e2669b86d42c739d3df163f873c51719552e263a3d85179297180fb4fa00a236", size = 472126, upload-time = "2026-03-31T17:52:40.174Z" }, + { url = "https://files.pythonhosted.org/packages/b7/4c/917d21d6619a4475cdafc6d13a69fdb3b901ddac57e76caca5a25c117b6d/anthropic-0.117.0-py3-none-any.whl", hash = "sha256:451a0a6905f11dff7663d13e4ee5dbf909eb8942b1d049803c7b937a13ac47ec", size = 998327, upload-time = "2026-07-16T19:36:11.225Z" }, ] [[package]] name = "anyio" -version = "4.13.0" +version = "4.14.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, { name = "idna" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/19/14/2c5dd9f512b66549ae92767a9c7b330ae88e1932ca57876909410251fe13/anyio-4.13.0.tar.gz", hash = "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc", size = 231622, upload-time = "2026-03-24T12:59:09.671Z" } +sdist = { url = "https://files.pythonhosted.org/packages/61/cc/a381afa6efea9f496eff839d4a6a1aed3bfafc7b3ab4b0d1b243a12573dd/anyio-4.14.2.tar.gz", hash = "sha256:cfa139f3ed1a23ee8f88a145ddb5ac7605b8bbfd8592baacd7ce3d8bb4313c7f", size = 260176, upload-time = "2026-07-12T20:29:07.082Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353, upload-time = "2026-03-24T12:59:08.246Z" }, + { url = "https://files.pythonhosted.org/packages/da/35/f2287558c17e29fafc8ef3daf819bb9834061cfa43bff8014f7df7f63bdc/anyio-4.14.2-py3-none-any.whl", hash = "sha256:9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494", size = 125813, upload-time = "2026-07-12T20:29:05.763Z" }, ] [[package]] name = "asttokens" -version = "3.0.1" +version = "3.0.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/be/a5/8e3f9b6771b0b408517c82d97aed8f2036509bc247d46114925e32fe33f0/asttokens-3.0.1.tar.gz", hash = "sha256:71a4ee5de0bde6a31d64f6b13f2293ac190344478f081c3d1bccfcf5eacb0cb7", size = 62308, upload-time = "2025-11-15T16:43:48.578Z" } +sdist = { url = "https://files.pythonhosted.org/packages/25/1e/faf0f247f6f881b98fc4d6d07e14085cb89d13665084e6d6ac1dc2c03d0b/asttokens-3.0.2.tar.gz", hash = "sha256:3ecdbd8f2cc195f53ccada3a613538bb5f9ef6f6869129f13e03c30a677b8fe2", size = 63136, upload-time = "2026-07-12T03:31:49.084Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/39/e7eaf1799466a4aef85b6a4fe7bd175ad2b1c6345066aa33f1f58d4b18d0/asttokens-3.0.1-py3-none-any.whl", hash = "sha256:15a3ebc0f43c2d0a50eeafea25e19046c68398e487b9f1f5b517f7c0f40f976a", size = 27047, upload-time = "2025-11-15T16:43:16.109Z" }, + { url = "https://files.pythonhosted.org/packages/d4/2b/04b8a15f3a1c77bc79ddf5c73875327f34b4fa75982df2b76e45e402d364/asttokens-3.0.2-py3-none-any.whl", hash = "sha256:9da13157f5b28becde0bd374fc677dcd3c290614264eff096f167c469cd9f933", size = 28702, upload-time = "2026-07-12T03:31:47.542Z" }, ] [[package]] @@ -137,15 +153,15 @@ wheels = [ [[package]] name = "azure-core" -version = "1.39.0" +version = "1.41.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "requests" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/34/83/bbde3faa84ddcb8eb0eca4b3ffb3221252281db4ce351300fe248c5c70b1/azure_core-1.39.0.tar.gz", hash = "sha256:8a90a562998dd44ce84597590fff6249701b98c0e8797c95fcdd695b54c35d74", size = 367531, upload-time = "2026-03-19T01:31:29.461Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a6/f3/b416179e408990df5db0d516283022dde0f5d0111d98c1a848e41853e81c/azure_core-1.41.0.tar.gz", hash = "sha256:f46ff5dfcd230f25cf1c19e8a34b8dc08a337b2503e268bb600a16c00db8ad5a", size = 381042, upload-time = "2026-05-07T23:30:54.302Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/d6/8ebcd05b01a580f086ac9a97fb9fac65c09a4b012161cc97c21a336e880b/azure_core-1.39.0-py3-none-any.whl", hash = "sha256:4ac7b70fab5438c3f68770649a78daf97833caa83827f91df9c14e0e0ea7d34f", size = 218318, upload-time = "2026-03-19T01:31:31.25Z" }, + { url = "https://files.pythonhosted.org/packages/5b/db/325c6d7312d2200251c52323878281045aaffcb5586612296484e4280eaa/azure_core-1.41.0-py3-none-any.whl", hash = "sha256:522b4011e8180b1a3dcd2024396a4e7fe9ac37fb8597db47163d230b5efe892d", size = 220920, upload-time = "2026-05-07T23:30:56.357Z" }, ] [[package]] @@ -193,7 +209,7 @@ wheels = [ [[package]] name = "black" -version = "26.3.1" +version = "26.5.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, @@ -205,34 +221,34 @@ dependencies = [ { name = "tomli", marker = "python_full_version < '3.11'" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e1/c5/61175d618685d42b005847464b8fb4743a67b1b8fdb75e50e5a96c31a27a/black-26.3.1.tar.gz", hash = "sha256:2c50f5063a9641c7eed7795014ba37b0f5fa227f3d408b968936e24bc0566b07", size = 666155, upload-time = "2026-03-12T03:36:03.593Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c0/37/5628dd55bf2b34257fc7603f0fe97c40e3aaf24265f416a9c85c95ca1436/black-26.5.1.tar.gz", hash = "sha256:dd321f668053961824bcc1be1cc1df748b2d7e4fa28086b08331e577b0100a73", size = 679439, upload-time = "2026-05-18T16:53:36.107Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/32/a8/11170031095655d36ebc6664fe0897866f6023892396900eec0e8fdc4299/black-26.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:86a8b5035fce64f5dcd1b794cf8ec4d31fe458cf6ce3986a30deb434df82a1d2", size = 1866562, upload-time = "2026-03-12T03:39:58.639Z" }, - { url = "https://files.pythonhosted.org/packages/69/ce/9e7548d719c3248c6c2abfd555d11169457cbd584d98d179111338423790/black-26.3.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5602bdb96d52d2d0672f24f6ffe5218795736dd34807fd0fd55ccd6bf206168b", size = 1703623, upload-time = "2026-03-12T03:40:00.347Z" }, - { url = "https://files.pythonhosted.org/packages/7f/0a/8d17d1a9c06f88d3d030d0b1d4373c1551146e252afe4547ed601c0e697f/black-26.3.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c54a4a82e291a1fee5137371ab488866b7c86a3305af4026bdd4dc78642e1ac", size = 1768388, upload-time = "2026-03-12T03:40:01.765Z" }, - { url = "https://files.pythonhosted.org/packages/52/79/c1ee726e221c863cde5164f925bacf183dfdf0397d4e3f94889439b947b4/black-26.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:6e131579c243c98f35bce64a7e08e87fb2d610544754675d4a0e73a070a5aa3a", size = 1412969, upload-time = "2026-03-12T03:40:03.252Z" }, - { url = "https://files.pythonhosted.org/packages/73/a5/15c01d613f5756f68ed8f6d4ec0a1e24b82b18889fa71affd3d1f7fad058/black-26.3.1-cp310-cp310-win_arm64.whl", hash = "sha256:5ed0ca58586c8d9a487352a96b15272b7fa55d139fc8496b519e78023a8dab0a", size = 1220345, upload-time = "2026-03-12T03:40:04.892Z" }, - { url = "https://files.pythonhosted.org/packages/17/57/5f11c92861f9c92eb9dddf515530bc2d06db843e44bdcf1c83c1427824bc/black-26.3.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:28ef38aee69e4b12fda8dba75e21f9b4f979b490c8ac0baa7cb505369ac9e1ff", size = 1851987, upload-time = "2026-03-12T03:40:06.248Z" }, - { url = "https://files.pythonhosted.org/packages/54/aa/340a1463660bf6831f9e39646bf774086dbd8ca7fc3cded9d59bbdf4ad0a/black-26.3.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bf9bf162ed91a26f1adba8efda0b573bc6924ec1408a52cc6f82cb73ec2b142c", size = 1689499, upload-time = "2026-03-12T03:40:07.642Z" }, - { url = "https://files.pythonhosted.org/packages/f3/01/b726c93d717d72733da031d2de10b92c9fa4c8d0c67e8a8a372076579279/black-26.3.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:474c27574d6d7037c1bc875a81d9be0a9a4f9ee95e62800dab3cfaadbf75acd5", size = 1754369, upload-time = "2026-03-12T03:40:09.279Z" }, - { url = "https://files.pythonhosted.org/packages/e3/09/61e91881ca291f150cfc9eb7ba19473c2e59df28859a11a88248b5cbbc4d/black-26.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:5e9d0d86df21f2e1677cc4bd090cd0e446278bcbbe49bf3659c308c3e402843e", size = 1413613, upload-time = "2026-03-12T03:40:10.943Z" }, - { url = "https://files.pythonhosted.org/packages/16/73/544f23891b22e7efe4d8f812371ab85b57f6a01b2fc45e3ba2e52ba985b8/black-26.3.1-cp311-cp311-win_arm64.whl", hash = "sha256:9a5e9f45e5d5e1c5b5c29b3bd4265dcc90e8b92cf4534520896ed77f791f4da5", size = 1219719, upload-time = "2026-03-12T03:40:12.597Z" }, - { url = "https://files.pythonhosted.org/packages/dc/f8/da5eae4fc75e78e6dceb60624e1b9662ab00d6b452996046dfa9b8a6025b/black-26.3.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b5e6f89631eb88a7302d416594a32faeee9fb8fb848290da9d0a5f2903519fc1", size = 1895920, upload-time = "2026-03-12T03:40:13.921Z" }, - { url = "https://files.pythonhosted.org/packages/2c/9f/04e6f26534da2e1629b2b48255c264cabf5eedc5141d04516d9d68a24111/black-26.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:41cd2012d35b47d589cb8a16faf8a32ef7a336f56356babd9fcf70939ad1897f", size = 1718499, upload-time = "2026-03-12T03:40:15.239Z" }, - { url = "https://files.pythonhosted.org/packages/04/91/a5935b2a63e31b331060c4a9fdb5a6c725840858c599032a6f3aac94055f/black-26.3.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f76ff19ec5297dd8e66eb64deda23631e642c9393ab592826fd4bdc97a4bce7", size = 1794994, upload-time = "2026-03-12T03:40:17.124Z" }, - { url = "https://files.pythonhosted.org/packages/e7/0a/86e462cdd311a3c2a8ece708d22aba17d0b2a0d5348ca34b40cdcbea512e/black-26.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:ddb113db38838eb9f043623ba274cfaf7d51d5b0c22ecb30afe58b1bb8322983", size = 1420867, upload-time = "2026-03-12T03:40:18.83Z" }, - { url = "https://files.pythonhosted.org/packages/5b/e5/22515a19cb7eaee3440325a6b0d95d2c0e88dd180cb011b12ae488e031d1/black-26.3.1-cp312-cp312-win_arm64.whl", hash = "sha256:dfdd51fc3e64ea4f35873d1b3fb25326773d55d2329ff8449139ebaad7357efb", size = 1230124, upload-time = "2026-03-12T03:40:20.425Z" }, - { url = "https://files.pythonhosted.org/packages/f5/77/5728052a3c0450c53d9bb3945c4c46b91baa62b2cafab6801411b6271e45/black-26.3.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:855822d90f884905362f602880ed8b5df1b7e3ee7d0db2502d4388a954cc8c54", size = 1895034, upload-time = "2026-03-12T03:40:21.813Z" }, - { url = "https://files.pythonhosted.org/packages/52/73/7cae55fdfdfbe9d19e9a8d25d145018965fe2079fa908101c3733b0c55a0/black-26.3.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8a33d657f3276328ce00e4d37fe70361e1ec7614da5d7b6e78de5426cb56332f", size = 1718503, upload-time = "2026-03-12T03:40:23.666Z" }, - { url = "https://files.pythonhosted.org/packages/e1/87/af89ad449e8254fdbc74654e6467e3c9381b61472cc532ee350d28cfdafb/black-26.3.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f1cd08e99d2f9317292a311dfe578fd2a24b15dbce97792f9c4d752275c1fa56", size = 1793557, upload-time = "2026-03-12T03:40:25.497Z" }, - { url = "https://files.pythonhosted.org/packages/43/10/d6c06a791d8124b843bf325ab4ac7d2f5b98731dff84d6064eafd687ded1/black-26.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:c7e72339f841b5a237ff14f7d3880ddd0fc7f98a1199e8c4327f9a4f478c1839", size = 1422766, upload-time = "2026-03-12T03:40:27.14Z" }, - { url = "https://files.pythonhosted.org/packages/59/4f/40a582c015f2d841ac24fed6390bd68f0fc896069ff3a886317959c9daf8/black-26.3.1-cp313-cp313-win_arm64.whl", hash = "sha256:afc622538b430aa4c8c853f7f63bc582b3b8030fd8c80b70fb5fa5b834e575c2", size = 1232140, upload-time = "2026-03-12T03:40:28.882Z" }, - { url = "https://files.pythonhosted.org/packages/d5/da/e36e27c9cebc1311b7579210df6f1c86e50f2d7143ae4fcf8a5017dc8809/black-26.3.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2d6bfaf7fd0993b420bed691f20f9492d53ce9a2bcccea4b797d34e947318a78", size = 1889234, upload-time = "2026-03-12T03:40:30.964Z" }, - { url = "https://files.pythonhosted.org/packages/0e/7b/9871acf393f64a5fa33668c19350ca87177b181f44bb3d0c33b2d534f22c/black-26.3.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f89f2ab047c76a9c03f78d0d66ca519e389519902fa27e7a91117ef7611c0568", size = 1720522, upload-time = "2026-03-12T03:40:32.346Z" }, - { url = "https://files.pythonhosted.org/packages/03/87/e766c7f2e90c07fb7586cc787c9ae6462b1eedab390191f2b7fc7f6170a9/black-26.3.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b07fc0dab849d24a80a29cfab8d8a19187d1c4685d8a5e6385a5ce323c1f015f", size = 1787824, upload-time = "2026-03-12T03:40:33.636Z" }, - { url = "https://files.pythonhosted.org/packages/ac/94/2424338fb2d1875e9e83eed4c8e9c67f6905ec25afd826a911aea2b02535/black-26.3.1-cp314-cp314-win_amd64.whl", hash = "sha256:0126ae5b7c09957da2bdbd91a9ba1207453feada9e9fe51992848658c6c8e01c", size = 1445855, upload-time = "2026-03-12T03:40:35.442Z" }, - { url = "https://files.pythonhosted.org/packages/86/43/0c3338bd928afb8ee7471f1a4eec3bdbe2245ccb4a646092a222e8669840/black-26.3.1-cp314-cp314-win_arm64.whl", hash = "sha256:92c0ec1f2cc149551a2b7b47efc32c866406b6891b0ee4625e95967c8f4acfb1", size = 1258109, upload-time = "2026-03-12T03:40:36.832Z" }, - { url = "https://files.pythonhosted.org/packages/8e/0d/52d98722666d6fc6c3dd4c76df339501d6efd40e0ff95e6186a7b7f0befd/black-26.3.1-py3-none-any.whl", hash = "sha256:2bd5aa94fc267d38bb21a70d7410a89f1a1d318841855f698746f8e7f51acd1b", size = 207542, upload-time = "2026-03-12T03:36:01.668Z" }, + { url = "https://files.pythonhosted.org/packages/be/84/b3f55026206a9e8820a91503308075ca48eadc515e436731ca01dbe043b3/black-26.5.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9942db8888e06943c5dde66ca0037dcff82a2a4ec1ad0ada9e0d2ee9d9823893", size = 1987719, upload-time = "2026-05-18T17:05:02.757Z" }, + { url = "https://files.pythonhosted.org/packages/c6/34/7db312c5e5783d6e76cffd9d5ac8972a32badae4c6e3288dac0eed8d3bed/black-26.5.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:89c93167a74d3a75dfaa38a5c7cca015537d5820dd7f17d63267d674a61cae90", size = 1810083, upload-time = "2026-05-18T17:05:04.302Z" }, + { url = "https://files.pythonhosted.org/packages/33/e2/e0101e73c2c8727634e2efcb35e2b34bd23ad70dfa673789f5773a591b21/black-26.5.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22f2cd76d069cc54c71f10360744ba8983fbb616903b4304a85b734915c8e1b4", size = 1860633, upload-time = "2026-05-18T17:05:06.391Z" }, + { url = "https://files.pythonhosted.org/packages/b0/4c/e15c0c5b23cf3651035fe5addcce90e283af3548a3f91bb03d81b83106ab/black-26.5.1-cp310-cp310-win_amd64.whl", hash = "sha256:87ed5c6f450580a2f6790bc7cbfb016dfc73bc750249762268a3695361315eef", size = 1477886, upload-time = "2026-05-18T17:05:07.96Z" }, + { url = "https://files.pythonhosted.org/packages/9f/3f/59d43ade98d2ce5c8dc34a4e46cbecd177e6d55d7d4092969c6003ccc655/black-26.5.1-cp310-cp310-win_arm64.whl", hash = "sha256:58b4bd92cf88aacf83d88479c8f9caee044b1ec55f2451a337354a7ea2590a22", size = 1277111, upload-time = "2026-05-18T17:05:09.473Z" }, + { url = "https://files.pythonhosted.org/packages/4b/96/3c3e09f09f44a37aac36b178a279cd19aa7001bd796187a7b162a294c81f/black-26.5.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:96ae2c733b2aabdd9986e2c5df628ff3473676cd1c5faded1ff496cf6d74083c", size = 1970639, upload-time = "2026-05-18T17:05:11.461Z" }, + { url = "https://files.pythonhosted.org/packages/83/ea/5ad117b9ee3ecd933c712bcbae610006e5b7cc9f41c526cd7ed3b6c4124c/black-26.5.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0e48b87e03bf109288e55cfceadcfa15ff5470aca2851a851950ed2926f450d7", size = 1792130, upload-time = "2026-05-18T17:05:12.983Z" }, + { url = "https://files.pythonhosted.org/packages/06/3a/7c448bc623fcdfa96672531beb5a616ea5e64f6975955254d7731ffb0ad9/black-26.5.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5119fa92ae61f786e8c3662fd60aece1d0a2dd5cca5d0c79417a95e7a4272a59", size = 1846134, upload-time = "2026-05-18T17:05:14.506Z" }, + { url = "https://files.pythonhosted.org/packages/a1/5b/0b39b3a5917f0657ac014ad2edb58c139553a478adfe7f817abf1622ff6e/black-26.5.1-cp311-cp311-win_amd64.whl", hash = "sha256:30d3c14661f2792e9142cce3eeeb1cbc175b3eb5f733be0c8eeb99651e52b0c3", size = 1478883, upload-time = "2026-05-18T17:05:16.542Z" }, + { url = "https://files.pythonhosted.org/packages/4c/48/dc222692e0f95030db1bbfb6c857e76858bad09058221ea7aae815255327/black-26.5.1-cp311-cp311-win_arm64.whl", hash = "sha256:1ef92b76f7733f282fd096ea406200b5a286c42947412b0eaff3a74e3616cefe", size = 1277776, upload-time = "2026-05-18T17:05:18.029Z" }, + { url = "https://files.pythonhosted.org/packages/24/99/7744b906703228264ef73bdd534df88ec1ef3de45c4e78f6d31b9e32d0c9/black-26.5.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4ad6fa01f941920f54f2bbb35f3df7673428a0ef98a0b0840c2eaef3b110efa8", size = 2012518, upload-time = "2026-05-18T17:05:20.108Z" }, + { url = "https://files.pythonhosted.org/packages/b7/c0/c5a3b1636dfd09c42534f2b3cf33506814f6d3e066fb0879ffa16c1ae860/black-26.5.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3915f256e75a2d7cf88d8953d37f780455dc586cc72dee059c528fe77f581217", size = 1816016, upload-time = "2026-05-18T17:05:21.84Z" }, + { url = "https://files.pythonhosted.org/packages/1f/0e/36044316b65ca471d3bb6d3703fd06fb50c6b727c3562f6a5a3153634f88/black-26.5.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d98d4137277c75dfb898ec8d846c4fd68ba1e9cf77f95e2865c203dc18f4c3d", size = 1884150, upload-time = "2026-05-18T17:05:23.546Z" }, + { url = "https://files.pythonhosted.org/packages/b3/33/dafc5808c2af43672912111d7c3354af1615f7e2be3bed7a878461abbe4d/black-26.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:a1dca32d9f1784af512a13410ec204c6f7f0aa9797a111c42e1c03449821c264", size = 1486825, upload-time = "2026-05-18T17:05:25.004Z" }, + { url = "https://files.pythonhosted.org/packages/82/14/b965ee6ad2a311f28bdbf692def3ee9848d2ae289dab28b27657fcee3e78/black-26.5.1-cp312-cp312-win_arm64.whl", hash = "sha256:1037d5ac7b7b310b2632ad867ec8d0e4c4819dcdb0b820f63135da746a24e418", size = 1288646, upload-time = "2026-05-18T17:05:26.477Z" }, + { url = "https://files.pythonhosted.org/packages/3f/5c/c384363980e11e25ca6b93205949bb331fbf35f4e0dbec376dfa6326cec8/black-26.5.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2b36cf2ddf5566e205f6535f782a62194a184d33e175b64ae8c40b1737522be3", size = 2009020, upload-time = "2026-05-18T17:05:28.132Z" }, + { url = "https://files.pythonhosted.org/packages/0b/df/9f31c5e0babbfed77d505fc5d120beb98b21b33feaeded3924ea941fe360/black-26.5.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1f7ea64ebfa01b50f693508fc39f875e264446d3b097088f84f203b9d09618a0", size = 1813335, upload-time = "2026-05-18T17:05:31.266Z" }, + { url = "https://files.pythonhosted.org/packages/fb/24/8e7b9a2fa61b0afd82209efe937557d180a1fa055bd7f6161eb9defc3719/black-26.5.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecb3e624844c798144e9bd986954e0adc81d8911a1f30f375e1252fe26e8c294", size = 1881614, upload-time = "2026-05-18T17:05:32.718Z" }, + { url = "https://files.pythonhosted.org/packages/49/ad/b4e0d9365ba8ac34f6bbab62a4b1b2dd5d618fac3fa1b8db968c844201b5/black-26.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:e1a26503279b6b310669fb0b219c39e4820b77e8189fe80f522bb511f247db0a", size = 1488925, upload-time = "2026-05-18T17:05:34.259Z" }, + { url = "https://files.pythonhosted.org/packages/a1/4b/652b859bf5df88a751c30451b09338f7fd26a77d1271c666992f836b7711/black-26.5.1-cp313-cp313-win_arm64.whl", hash = "sha256:5c34b25da232ead53a6f335b76dbea124f4d152ad568b9080d6f944bc2b34b52", size = 1289883, upload-time = "2026-05-18T17:05:36.019Z" }, + { url = "https://files.pythonhosted.org/packages/a6/16/a8da8eb208c51c7f4ce74609a45d0dcc6d8a2141e45e81ee5289d1bb0d59/black-26.5.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:e88976690a64b0af98312ca958415849cb42423423c5f2ee74af4b49a97a2168", size = 2004800, upload-time = "2026-05-18T17:05:38.182Z" }, + { url = "https://files.pythonhosted.org/packages/11/8a/a479296a19e383b70a725882a6cf3d786540601ff03cabbaaf1cce864c5a/black-26.5.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:32d5ea7f6c8bdfa6e648326ebca1f02b0764e2a029edc6f8dce2627e19d468c3", size = 1815576, upload-time = "2026-05-18T17:05:40.309Z" }, + { url = "https://files.pythonhosted.org/packages/81/6b/cfaf3d39f25132c156a068f6b805576c9103a84086019507c70e1911ee7d/black-26.5.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ea8d16dc41655aa113cd64665e7219446cd7e4ff2248d7178eaa905190c86b18", size = 1877927, upload-time = "2026-05-18T17:05:42.463Z" }, + { url = "https://files.pythonhosted.org/packages/66/76/302e313964bcff7e28df329d39f84f5270095730d85ff0acc260610a0d82/black-26.5.1-cp314-cp314-win_amd64.whl", hash = "sha256:577f21094ea469ef92ec1adaf2c9441a226d2144d01a5be2fa823cecf6543e50", size = 1511860, upload-time = "2026-05-18T17:05:43.943Z" }, + { url = "https://files.pythonhosted.org/packages/27/4e/a3827e35e0e567f9f9ee59e2a0ab979267dca98718f25547ca8c6733afd4/black-26.5.1-cp314-cp314-win_arm64.whl", hash = "sha256:ed1a20af114c301a0269bf01163d51dbef72737fd65f850001e7cbe7f3c7abae", size = 1316632, upload-time = "2026-05-18T17:05:45.521Z" }, + { url = "https://files.pythonhosted.org/packages/94/51/f975cae76d44274cc2868dc9040ac5d58d464784610234455b4e7b19c6ef/black-26.5.1-py3-none-any.whl", hash = "sha256:4ed7f7da04046d2e488437170797d3b4a4ad83906683bcb7dfc68b673bbce5e2", size = 213693, upload-time = "2026-05-18T16:53:33.964Z" }, ] [[package]] @@ -253,11 +269,11 @@ wheels = [ [[package]] name = "cachetools" -version = "7.0.5" +version = "7.1.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/af/dd/57fe3fdb6e65b25a5987fd2cdc7e22db0aef508b91634d2e57d22928d41b/cachetools-7.0.5.tar.gz", hash = "sha256:0cd042c24377200c1dcd225f8b7b12b0ca53cc2c961b43757e774ebe190fd990", size = 37367, upload-time = "2026-03-09T20:51:29.451Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f4/8b/0d3945a13955303b81272f759a0331e54c5c793da455e6f5706b89d2639c/cachetools-7.1.4.tar.gz", hash = "sha256:437f55a4e0c1b01a4f3077cc470e6991d47430970e36fbcb77e2be0df4fc1cd6", size = 40085, upload-time = "2026-05-21T22:40:43.376Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/06/f3/39cf3367b8107baa44f861dc802cbf16263c945b62d8265d36034fc07bea/cachetools-7.0.5-py3-none-any.whl", hash = "sha256:46bc8ebefbe485407621d0a4264b23c080cedd913921bad7ac3ed2f26c183114", size = 13918, upload-time = "2026-03-09T20:51:27.33Z" }, + { url = "https://files.pythonhosted.org/packages/8c/7b/1fc1c09cc0756cf25861a3be10565915953876da48bb228fb9a672b20a42/cachetools-7.1.4-py3-none-any.whl", hash = "sha256:323dc4127934744db5b54eb4924482d7edafbf9554e820d1531c2e08c0e4ef54", size = 16761, upload-time = "2026-05-21T22:40:41.845Z" }, ] [[package]] @@ -291,210 +307,220 @@ wheels = [ [[package]] name = "certifi" -version = "2026.2.25" +version = "2026.6.17" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", size = 155029, upload-time = "2026-02-25T02:54:17.342Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c9/c7/424b75da314c1045981bd9777432fad05a9e0c69daa4ed7e308bbaffe405/certifi-2026.6.17.tar.gz", hash = "sha256:024c88eeec92ca068db80f02b8b07c9cef7b9fe261d1d535abfd5abd6f6af432", size = 134594, upload-time = "2026-06-17T10:31:07.894Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" }, + { url = "https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl", hash = "sha256:2227dcbaafe0d2f59279d1762ddddc37783ed4354594f194ffc31d20f41fc3db", size = 133289, upload-time = "2026-06-17T10:31:06.348Z" }, ] [[package]] name = "cffi" -version = "2.0.0" +version = "2.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pycparser", marker = "implementation_name != 'PyPy'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } +sdist = { url = "https://files.pythonhosted.org/packages/57/5f/ff100cae70ebe9d8df1c01a00e510e45d9adb5c1fdda84791b199141de97/cffi-2.1.0.tar.gz", hash = "sha256:efc1cdd798b1aaf39b4610bba7aad28c9bea9b910f25c784ccf9ec1fa719d1f9", size = 531036, upload-time = "2026-07-06T21:34:30.382Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/93/d7/516d984057745a6cd96575eea814fe1edd6646ee6efd552fb7b0921dec83/cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44", size = 184283, upload-time = "2025-09-08T23:22:08.01Z" }, - { url = "https://files.pythonhosted.org/packages/9e/84/ad6a0b408daa859246f57c03efd28e5dd1b33c21737c2db84cae8c237aa5/cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49", size = 180504, upload-time = "2025-09-08T23:22:10.637Z" }, - { url = "https://files.pythonhosted.org/packages/50/bd/b1a6362b80628111e6653c961f987faa55262b4002fcec42308cad1db680/cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c", size = 208811, upload-time = "2025-09-08T23:22:12.267Z" }, - { url = "https://files.pythonhosted.org/packages/4f/27/6933a8b2562d7bd1fb595074cf99cc81fc3789f6a6c05cdabb46284a3188/cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb", size = 216402, upload-time = "2025-09-08T23:22:13.455Z" }, - { url = "https://files.pythonhosted.org/packages/05/eb/b86f2a2645b62adcfff53b0dd97e8dfafb5c8aa864bd0d9a2c2049a0d551/cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0", size = 203217, upload-time = "2025-09-08T23:22:14.596Z" }, - { url = "https://files.pythonhosted.org/packages/9f/e0/6cbe77a53acf5acc7c08cc186c9928864bd7c005f9efd0d126884858a5fe/cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4", size = 203079, upload-time = "2025-09-08T23:22:15.769Z" }, - { url = "https://files.pythonhosted.org/packages/98/29/9b366e70e243eb3d14a5cb488dfd3a0b6b2f1fb001a203f653b93ccfac88/cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453", size = 216475, upload-time = "2025-09-08T23:22:17.427Z" }, - { url = "https://files.pythonhosted.org/packages/21/7a/13b24e70d2f90a322f2900c5d8e1f14fa7e2a6b3332b7309ba7b2ba51a5a/cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495", size = 218829, upload-time = "2025-09-08T23:22:19.069Z" }, - { url = "https://files.pythonhosted.org/packages/60/99/c9dc110974c59cc981b1f5b66e1d8af8af764e00f0293266824d9c4254bc/cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5", size = 211211, upload-time = "2025-09-08T23:22:20.588Z" }, - { url = "https://files.pythonhosted.org/packages/49/72/ff2d12dbf21aca1b32a40ed792ee6b40f6dc3a9cf1644bd7ef6e95e0ac5e/cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb", size = 218036, upload-time = "2025-09-08T23:22:22.143Z" }, - { url = "https://files.pythonhosted.org/packages/e2/cc/027d7fb82e58c48ea717149b03bcadcbdc293553edb283af792bd4bcbb3f/cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a", size = 172184, upload-time = "2025-09-08T23:22:23.328Z" }, - { url = "https://files.pythonhosted.org/packages/33/fa/072dd15ae27fbb4e06b437eb6e944e75b068deb09e2a2826039e49ee2045/cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739", size = 182790, upload-time = "2025-09-08T23:22:24.752Z" }, - { url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344, upload-time = "2025-09-08T23:22:26.456Z" }, - { url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560, upload-time = "2025-09-08T23:22:28.197Z" }, - { url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" }, - { url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" }, - { url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374, upload-time = "2025-09-08T23:22:32.507Z" }, - { url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597, upload-time = "2025-09-08T23:22:34.132Z" }, - { url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574, upload-time = "2025-09-08T23:22:35.443Z" }, - { url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" }, - { url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload-time = "2025-09-08T23:22:38.436Z" }, - { url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" }, - { url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076, upload-time = "2025-09-08T23:22:40.95Z" }, - { url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820, upload-time = "2025-09-08T23:22:42.463Z" }, - { url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635, upload-time = "2025-09-08T23:22:43.623Z" }, - { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" }, - { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" }, - { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, - { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" }, - { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" }, - { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" }, - { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" }, - { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" }, - { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" }, - { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" }, - { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" }, - { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" }, - { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, - { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, - { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, - { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, - { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, - { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, - { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, - { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, - { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, - { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, - { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, - { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, - { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, - { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, - { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, - { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, - { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, - { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, - { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, - { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, - { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, - { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, - { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, - { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, - { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, - { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, - { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, - { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, - { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, - { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, - { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, - { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, - { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, - { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, + { url = "https://files.pythonhosted.org/packages/c0/e9/6d7724983b3d5a0908dbf74f64038ade77c18646ff6636ec7894fd392ce1/cffi-2.1.0-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:b65f590ef2a44640f9a05dbb548a429b4ade77913ce683ac8b1480777658a6c0", size = 183837, upload-time = "2026-07-06T21:32:09.655Z" }, + { url = "https://files.pythonhosted.org/packages/69/aa/24580a278de21fd7322635556334d9b535f1cbc00b0a3919447cdf464c65/cffi-2.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:164bff1657b2a74f0b6d54e11c9b375bc97b931f2ca9c43fcf875838da1570dd", size = 184226, upload-time = "2026-07-06T21:32:11.196Z" }, + { url = "https://files.pythonhosted.org/packages/88/a9/02cae418ec4beb282ace11958d9d4737793439d561fadc7e6d56f2e2b354/cffi-2.1.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:c941bb58d5a6e1c3892d86e42927ed6c180302f07e6d395d08c416e594b98b46", size = 211107, upload-time = "2026-07-06T21:32:12.328Z" }, + { url = "https://files.pythonhosted.org/packages/3b/30/c806937ed5e4c2c7ac30d9d6b76b5dc57ff8b75d83800d9bb11a8253cf2a/cffi-2.1.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a016194dbe13d14ee9556e734b772d8d67b947092b268d757fd4290e3ba2dfc2", size = 218733, upload-time = "2026-07-06T21:32:13.67Z" }, + { url = "https://files.pythonhosted.org/packages/f9/cf/398272b8bbfd58aa314fda5a7f1cdbb26d1d78ae324a11211521315dd1f0/cffi-2.1.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:03e9810d18c646077e501f661b682fbf5dee4676048527ca3cffe66faa9960dd", size = 205543, upload-time = "2026-07-06T21:32:15.148Z" }, + { url = "https://files.pythonhosted.org/packages/45/ca/f91641185cdd90c36d317a9dc7f85e88ef8682d8b300977baff5e23c35d8/cffi-2.1.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:19c54ac121cad98450b4896fa9a43ee0180d57bc4bc911a33db6cab1efab6cd3", size = 205460, upload-time = "2026-07-06T21:32:16.479Z" }, + { url = "https://files.pythonhosted.org/packages/38/66/04781a77b411f0bb5b234d62c1814754ab75ebe455ccff1b08e8d7aae98f/cffi-2.1.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d433a51f1870e43a13b6732f92aaf540ff77c2015097c78556f75a2d6c030e0", size = 218760, upload-time = "2026-07-06T21:32:17.98Z" }, + { url = "https://files.pythonhosted.org/packages/d0/9a/bb1d5ed9c3fcae158e9f6391bf309c95d98c2ac37ed56573228471d0af5e/cffi-2.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3d7f118b5adbfdfead90c25822690b02bc8074fba949bb7858bec4ebd55adb43", size = 221230, upload-time = "2026-07-06T21:32:19.407Z" }, + { url = "https://files.pythonhosted.org/packages/41/aa/3c1409cdd26094efacd1c36c66e0a6eb9d4296e4fd4f9901b8b2042f4323/cffi-2.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:c5f5df567f6eb216de69be06ce55c8b714090fae02b18a3b40da8163b8c5fa9c", size = 213524, upload-time = "2026-07-06T21:32:20.828Z" }, + { url = "https://files.pythonhosted.org/packages/fa/75/74dfb7c3fc6ebbd408038476bd4c1d7e925c62614e7b9c534ecc34218288/cffi-2.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:11b3fb55f4f8ad92274ed26705f65d8f91457de71f5380061eb6d125a768fecd", size = 220341, upload-time = "2026-07-06T21:32:21.9Z" }, + { url = "https://files.pythonhosted.org/packages/70/b6/9003c33a3e7d2c1306f5962e646457dcfe5a8cd8fce6bbe02d7af25db783/cffi-2.1.0-cp310-cp310-win32.whl", hash = "sha256:9d72af0cf10a76a600a9690078fe31c63b9588c8e86bf9fd353f713c84b5db0f", size = 174578, upload-time = "2026-07-06T21:32:23.073Z" }, + { url = "https://files.pythonhosted.org/packages/8a/26/710688310447531c7a22f857c7f79d9855ec18b03e04494ced723fb37e2f/cffi-2.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:fb62edb5bb52cca65fab91a63afa7561607120d26090a7e8fda6fb9f064726da", size = 185071, upload-time = "2026-07-06T21:32:24.671Z" }, + { url = "https://files.pythonhosted.org/packages/d3/67/85c89a59ba36a671e79638f44d466749f08179266a57e4f2ffdf92174072/cffi-2.1.0-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:02cb7ff33ded4f1532476731f89ede53e2e488a8e6205515a82144246ffa7dcc", size = 183845, upload-time = "2026-07-06T21:32:26.32Z" }, + { url = "https://files.pythonhosted.org/packages/ea/dd/e3b0baa2d3d6a857ac72b7efbf18e32e487c9cdafcc13049ad765495b15e/cffi-2.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f5bce581e6b8c235e566a14768a943b172ada3ed73537bb0c0be1edee312d4e7", size = 184186, upload-time = "2026-07-06T21:32:28.025Z" }, + { url = "https://files.pythonhosted.org/packages/65/68/9f3ef890cf3c6ab97bd531c5677f67613d302165d16f8142b2811782a614/cffi-2.1.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:30b65779d598c370374fefabf138d456fd6f3216bfa7bedfab1ba82025b0cd93", size = 211892, upload-time = "2026-07-06T21:32:29.565Z" }, + { url = "https://files.pythonhosted.org/packages/22/d7/1a74539db16d8bfd839ff1515948948efbb162e574650fd3d846896eea95/cffi-2.1.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:88023dfe18799507b73f1dbb0d14326a17465de1bc9c9c7655c22845e9ddc3a2", size = 218793, upload-time = "2026-07-06T21:32:30.951Z" }, + { url = "https://files.pythonhosted.org/packages/ec/d1/9a5b7169499e8e8d8e636de70b97ac7c9447104d2ff1a2cd94790cea5162/cffi-2.1.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:0a96b74cda968eebbad56d973efe5098974f0a9fb323865bf99ea1fd24e3e64c", size = 205737, upload-time = "2026-07-06T21:32:32.216Z" }, + { url = "https://files.pythonhosted.org/packages/ba/b0/e131a9c41f10607926278453d9596163594fe1c4ebc46efe3b5e5b34eb84/cffi-2.1.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a5781494d4d400a3f47f8f1da94b324f6e6b440a53387774002890a2a2f4b50f", size = 204909, upload-time = "2026-07-06T21:32:33.655Z" }, + { url = "https://files.pythonhosted.org/packages/fb/d2/4398416cd699b35167947c6e22aca52c47e69ad5695073c9f1f2c52e04aa/cffi-2.1.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aa7a1b53a2a4452ada2d1b5dade9960b2522f1e61293a811a077439e39029565", size = 217883, upload-time = "2026-07-06T21:32:35.173Z" }, + { url = "https://files.pythonhosted.org/packages/a2/a5/d4fe77b589e5e82d43ebc809bf2e6474afe8e48e32ea050b9357645b6471/cffi-2.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9d8272c0e483b024e1b9ad029821470ed8ec65631dbd90217469da0e7cd89f1c", size = 221251, upload-time = "2026-07-06T21:32:36.527Z" }, + { url = "https://files.pythonhosted.org/packages/22/f0/a2fc43084c0433caf7f461bccc013e28f848d04ee1c5ed7fce71423cf4d9/cffi-2.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:7762faa47e8ff7eb80bd261d9a7d8eea2d8baa69de5e95b70c1f338bbe712f02", size = 214250, upload-time = "2026-07-06T21:32:37.852Z" }, + { url = "https://files.pythonhosted.org/packages/04/8c/b925975448cf20634a9fbd5efceb807219db452653648d2897c0989cab2d/cffi-2.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:89095c1968b4ba8285840e131bf2891b09ae137fe2146905acae0354fbce1b5e", size = 219441, upload-time = "2026-07-06T21:32:39.146Z" }, + { url = "https://files.pythonhosted.org/packages/eb/da/5c4918a2d61d86fa927d716cb3d8e4626ef8dc8f605a599d32f33897f59a/cffi-2.1.0-cp311-cp311-win32.whl", hash = "sha256:64c753a0f87a256020004f37a1c8c02c480e725f910f0b2a0f3f07debd1b2479", size = 174496, upload-time = "2026-07-06T21:32:40.467Z" }, + { url = "https://files.pythonhosted.org/packages/f9/c8/6c2de1d55cf35ef8b92885d5ef280790f0fb9634d87ea1cc315176aecd61/cffi-2.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:4f26194e3d95e06501b942642855aed4f953d55e95d7d01b7c4483db3ecff458", size = 185113, upload-time = "2026-07-06T21:32:41.761Z" }, + { url = "https://files.pythonhosted.org/packages/9e/4e/e8d7cb5783f1841a3c8fb3a7735838d7484d08ec08c9f984b14cac1ac0e9/cffi-2.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:35aaea0c7ee0e58a5cd8c2fd1a48fdf7ece0d2699b7ecdda08194e9ce5dd9b3d", size = 179927, upload-time = "2026-07-06T21:32:42.961Z" }, + { url = "https://files.pythonhosted.org/packages/1e/85/990925db5df586ec90beb97529c853497e7f85ba0234830447faf41c3057/cffi-2.1.0-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:df2b82571a1b30f58a87bf4e5a9e78d2b1eff6c6ce8fd3aa3757221f93f0863f", size = 184829, upload-time = "2026-07-06T21:32:44.324Z" }, + { url = "https://files.pythonhosted.org/packages/4b/92/e7bb136ad6b5352603732cf907ef862ca103f20f2031c1735a46300c20c9/cffi-2.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:78474632761faa0fb96f30b1c928c84ebcf68713cbb80d15bab09dfe61640fde", size = 184728, upload-time = "2026-07-06T21:32:45.683Z" }, + { url = "https://files.pythonhosted.org/packages/c3/c0/d1ec30ffb370f748f2fb54425972bfef9871e0132e82fb589c46b6676049/cffi-2.1.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:5972433ad71a9e46516584ef60a0fda12d9dc459938d1539c3ddecf9bdc1368d", size = 214815, upload-time = "2026-07-06T21:32:48.557Z" }, + { url = "https://files.pythonhosted.org/packages/1b/dc/5620cf930688be01f2d673804291de757a934c90b946dbdc3d84130c2ea4/cffi-2.1.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b6422532152adf4e59b110cb2808cee7a033800952f5c036b4af047ee43199e7", size = 222429, upload-time = "2026-07-06T21:32:49.848Z" }, + { url = "https://files.pythonhosted.org/packages/4b/a4/77b53abbf7a1e0beb9637edbef2a94d15f9c822f591e85d439ffd91519a6/cffi-2.1.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:46b1c8db8f6122420f32d02fffb924c2fe9bc772d228c7c711748fff56aabb2b", size = 210315, upload-time = "2026-07-06T21:32:51.221Z" }, + { url = "https://files.pythonhosted.org/packages/58/0c/f528df19cc94b675087324d4760d9e6d5bfae97d6217aa4fac43de4f5fcc/cffi-2.1.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9fafc5aa2e2a39aaf7f8cc0c1f044a9b07fca12e558dca53a3cc5c654ad67a7", size = 208859, upload-time = "2026-07-06T21:32:52.512Z" }, + { url = "https://files.pythonhosted.org/packages/62/f2/c9522a81c32132799a1972c39f5c5f8b4c8b9f00488a23feaa6c06f07741/cffi-2.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1e9f50d192a3e525b15a75ab5114e442d83d657b7ec29182a991bc9a88fd3a66", size = 221844, upload-time = "2026-07-06T21:32:53.704Z" }, + { url = "https://files.pythonhosted.org/packages/6e/28/bd53988b9833e8f8ad539d26f4c07a6b3f6bcb1e9e02e7ca038250b3428d/cffi-2.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:98fff996e983a36d3aa2eca83af40c5821202e7e6f32d13ae94e3d2286f10cfe", size = 225287, upload-time = "2026-07-06T21:32:54.907Z" }, + { url = "https://files.pythonhosted.org/packages/79/99/0d0fd37f055224085f42bbb2c022d002e17dde4a97972822327b07d84101/cffi-2.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:379de10ce1ba048b1448599d1b37b24caee16309d1ac98d3982fc997f768700b", size = 223681, upload-time = "2026-07-06T21:32:56.329Z" }, + { url = "https://files.pythonhosted.org/packages/b0/80/c138990aa2a70b1a269f6e06348729836d733d6f970867943f61d367f8cc/cffi-2.1.0-cp312-cp312-win32.whl", hash = "sha256:9b8f0f26ca4e7513c534d351eca551947d053fac438f2a04ac96d882909b0d3a", size = 175269, upload-time = "2026-07-06T21:32:57.777Z" }, + { url = "https://files.pythonhosted.org/packages/a8/eb/f636456ff21a83fc13c032b58cc5dde061691546ac79efa284b2989b7982/cffi-2.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:c97f080ea627e2863524c5af3836e2270b5f5dfff1f104392b959f8df0c5d384", size = 185881, upload-time = "2026-07-06T21:32:59.253Z" }, + { url = "https://files.pythonhosted.org/packages/dd/2c/400ea43e721727dca8a65c4521390e9196757caba4a45643acb2b63271b8/cffi-2.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:6d194185eabd279f1c05ebe3504265ddfc5ad2b58d0714f7db9f01da592e9eb6", size = 180088, upload-time = "2026-07-06T21:33:02.278Z" }, + { url = "https://files.pythonhosted.org/packages/96/88/a996879e2eeccb815f6e3a5967b12a308257412acec882039d386bd2aa7b/cffi-2.1.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:10537b1df4967ca26d21e5072d7d54188354483b91dc75058968d3f0cf13fbda", size = 194331, upload-time = "2026-07-06T21:33:03.697Z" }, + { url = "https://files.pythonhosted.org/packages/58/85/7ae00d5c8dd6266f4e944c3db630f3c5c9a98b61d469c714d848b1d8138a/cffi-2.1.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:a95b05f9baf29b91171b3a8bd2020b028835243e7b0ff6bb23e2a3c228518b1b", size = 196966, upload-time = "2026-07-06T21:33:05.353Z" }, + { url = "https://files.pythonhosted.org/packages/8c/e9/45c3a76ad8d43ad9261f4c95436da61128d3ca545d72b9612c0ab5be0b1c/cffi-2.1.0-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:15faec4adfff450819f3aee0e2e02c812de6edb88203aa58807955db2003472a", size = 184795, upload-time = "2026-07-06T21:33:06.699Z" }, + { url = "https://files.pythonhosted.org/packages/84/4c/82f132cb4418ee6d953d982b19191e87e2a6372c8a4ce36e50b69d6ade4a/cffi-2.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:716ff8ec22f20b4d988b12884086bcef0fc99737043e503f7a3935a6be99b1ea", size = 184746, upload-time = "2026-07-06T21:33:08.071Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1c/4ed5a0e5bdca6cbc275556de3328dd1b76fd0c11cc13c88fe66d1d8715f2/cffi-2.1.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:63960549e4f8dc41e31accb97b975abaecfc44c03e396c093a6436763c2ea7db", size = 214747, upload-time = "2026-07-06T21:33:09.671Z" }, + { url = "https://files.pythonhosted.org/packages/3a/a6/e879bb68cc23a2bc9ba8f4b7d8019f0c2694bad2ab6c4a3701d429439f58/cffi-2.1.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ff067a8d8d880e7809e4ac88eb009bb848870115317b306666502ccad30b147f", size = 222392, upload-time = "2026-07-06T21:33:10.896Z" }, + { url = "https://files.pythonhosted.org/packages/88/f6/01890cfd63c08f8eb96a8319b0443690197d240a8bd6346048cf7bde9190/cffi-2.1.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:3b926723c13eba9f81d2ef3820d63aeceec3b2d4639906047bf675cb8a7a500d", size = 210285, upload-time = "2026-07-06T21:33:12.251Z" }, + { url = "https://files.pythonhosted.org/packages/a6/cf/2b684132056f438567b61e19d690dd31cd0921ace051e0a458be6074369e/cffi-2.1.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:47ff3a8bfd8cb9da1af7524b965127095055654c177fcfc7578debcb015eecd0", size = 208801, upload-time = "2026-07-06T21:33:13.617Z" }, + { url = "https://files.pythonhosted.org/packages/6f/08/f2e7d62c460faae0926f2d6e423694aa409ced3bc1fe2927a0a6e5f05416/cffi-2.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:799416bae98336e400981ff6e532d67d5c709cfb30afb79865a1315f94b0e224", size = 221808, upload-time = "2026-07-06T21:33:15.466Z" }, + { url = "https://files.pythonhosted.org/packages/38/37/04f54b8e63a02f3d908332c9effbf8c366167c6f733ed8a3d4f79b7e2a1e/cffi-2.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:961be50688f7fba2fa65f63712d3b9b341a22311f5253460ce933f52f0de1c8c", size = 225241, upload-time = "2026-07-06T21:33:16.869Z" }, + { url = "https://files.pythonhosted.org/packages/a9/d6/c72eecca433cd3e681c65ed313ab4835d9d4a379704d0f628a6a05f51c2e/cffi-2.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bf5c6cf48238b0eb4c086978c492ad1cbc22373fc5b2d7353b3a598ce6db887a", size = 223588, upload-time = "2026-07-06T21:33:18.239Z" }, + { url = "https://files.pythonhosted.org/packages/c6/4b/e706f67279140f92939da3475ad610df18bfd52d50f14953a8e5fede71d5/cffi-2.1.0-cp313-cp313-win32.whl", hash = "sha256:db3eb7d46527159a878ec3460e9d40615bc25ba337d477db681aea6e4f05c5d2", size = 175248, upload-time = "2026-07-06T21:33:19.799Z" }, + { url = "https://files.pythonhosted.org/packages/5a/47/59eb7975cb0e4ef0afa764ea945b29a5bb4537a9f771cb7d6c8a5dd74c95/cffi-2.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:8e74a6135550c4748af665b1b1118b6aab33b1fc6a16f9aff630af107c3b4512", size = 185717, upload-time = "2026-07-06T21:33:21.47Z" }, + { url = "https://files.pythonhosted.org/packages/5a/af/34fee85c48f8d94efc8597bc09470c9dd274c145f1c12e0fbc6ab6d38d74/cffi-2.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:2282cd5e38aa8accd03e99d1256af8411c84cdbee6a89d841b563fdbd1f3e50f", size = 180114, upload-time = "2026-07-06T21:33:22.515Z" }, + { url = "https://files.pythonhosted.org/packages/d8/f0/81478e482afa03f6d18dc8f2afb5edc45b3080853b634b5ed91961be0998/cffi-2.1.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:d2117334c3af3bdcb9a88522b844a2bdb5efdc4f71c6c822df55486ae1c3347a", size = 194142, upload-time = "2026-07-06T21:33:23.657Z" }, + { url = "https://files.pythonhosted.org/packages/7d/95/8de304305cd9204974b0ca051b86d307cafca13aa575a0ef1b44d92c0d8c/cffi-2.1.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:702c436735fbe99d59ada02a1f65cfc0d31c0ee8b7290912f8fbc5cd1e4b16c3", size = 196819, upload-time = "2026-07-06T21:33:25.007Z" }, + { url = "https://files.pythonhosted.org/packages/20/71/7c8372d30e42415602ed9f268f7cfd66f1b855fed881ecd168bcb45dbc0b/cffi-2.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:1ff3456eab0d889592d1936d6125bbfbc7ae4d3354a700f8bd80450a66445d4d", size = 184965, upload-time = "2026-07-06T21:33:26.605Z" }, + { url = "https://files.pythonhosted.org/packages/d6/5c/584e626835f0375c928176c04137c96927165cb8733cdb3150ec04e5ee5e/cffi-2.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c4165821e131d6d4ca444347c2b694e2311bcfa3fe5a861cc72968f28867beac", size = 184952, upload-time = "2026-07-06T21:33:27.823Z" }, + { url = "https://files.pythonhosted.org/packages/2e/d2/065fcae1c73979fac8e054462478d0ff8a29c40cdc2ed7ea5676a061df53/cffi-2.1.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:276f20fffd7b396e12516ba8edf9509210ac248cbbc5acbc39cd512f9f59ebe6", size = 222353, upload-time = "2026-07-06T21:33:29.178Z" }, + { url = "https://files.pythonhosted.org/packages/ed/a5/e8bbb1ce5b3ac2f53ad6a10bde44318a5a8d99d4f4a000d44a6e39aeb3e4/cffi-2.1.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:7d5980a3433d4b71a5e120f9dd551403d7824e31e2e67124fe2769c404c06913", size = 210051, upload-time = "2026-07-06T21:33:30.534Z" }, + { url = "https://files.pythonhosted.org/packages/28/ed/c127d3ac36e899c965e3361357c3befacd6578c03f40125183e41c3b219e/cffi-2.1.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:6ca4919c6e4f89aa99c42510b42cf54596892c00b3f9077f6bdd1505e24b9c8d", size = 208630, upload-time = "2026-07-06T21:33:31.753Z" }, + { url = "https://files.pythonhosted.org/packages/cc/d7/97d3136f81db489ec8d1d67748c110d6c994268fd7528014aa9f2b085e4e/cffi-2.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d53d10f7da99ae46f7373b9150393e9c5eab9b224909982b43832668de4779f5", size = 221593, upload-time = "2026-07-06T21:33:33.044Z" }, + { url = "https://files.pythonhosted.org/packages/d3/27/93195977168ee63aed233a1a0993a2178798654d1f4bddcdd321d6fd3b21/cffi-2.1.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c351efb95e832a853a29361675f33a7ce53de1a109cd73fd47af0712213aa4ce", size = 225146, upload-time = "2026-07-06T21:33:34.224Z" }, + { url = "https://files.pythonhosted.org/packages/b3/c1/6dbd291ee2ae5a50a034aa057207081f545923bbf15dad4511e985aafff5/cffi-2.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:dbf7c7a88e2bac086f06d14577332760bdeecc42bdec8ac4077f6260557d9326", size = 223240, upload-time = "2026-07-06T21:33:35.57Z" }, + { url = "https://files.pythonhosted.org/packages/0f/6f/ade5ce9863a57992a6ea3d0d10d7e29b8749fc127204b3d493d667b2815f/cffi-2.1.0-cp314-cp314-win32.whl", hash = "sha256:1854b724d00f6654c742097d5387569021be12d3a0f770eae1df8f8acfcc6acd", size = 177723, upload-time = "2026-07-06T21:33:51.626Z" }, + { url = "https://files.pythonhosted.org/packages/41/de/92b9eeed4ae4a21d6fd9b2a2c8505cbed573299902ea73981cc13f7ff62c/cffi-2.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:1b96bfe2c4bd825681b7d311ad6d9b7280a091f43e8f63da5729638083cd3bfb", size = 187937, upload-time = "2026-07-06T21:33:53.403Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1a/cc6ae6c2913a03aab8898eee57963cf1035b8df5872ed8b9115fcc7e2be8/cffi-2.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:7d28dff1db6764108bc30788d85d61c876beff416d9a49cb9dd7c5a9f34f5804", size = 183001, upload-time = "2026-07-06T21:33:54.74Z" }, + { url = "https://files.pythonhosted.org/packages/14/f0/134c00ce0779ec86dea2aa1aac69339c2741a8045072676763512363a2ea/cffi-2.1.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7ea6b3e2c4250ff1de21c630fe72d0f63eb95c2c32ffbf64a358cf4a8836d714", size = 188538, upload-time = "2026-07-06T21:33:36.792Z" }, + { url = "https://files.pythonhosted.org/packages/50/d8/3b86aba791cb610d24e8a3e1b2cd529e71fa15096b04e4d4e360049d4a4c/cffi-2.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6af371f3767faeffc6ac1ef57cdfd25844403e9d3f476c5537caee499de96376", size = 188230, upload-time = "2026-07-06T21:33:38.011Z" }, + { url = "https://files.pythonhosted.org/packages/14/d0/117dcd9209255ad8571fbc8c92ef32593a1d294dcec91ddc4e4db50606f2/cffi-2.1.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:eb4e8997a49aa2c08a3e43c9045d224448b8941d88e7ac163c7d383e560cbf98", size = 223899, upload-time = "2026-07-06T21:33:39.514Z" }, + { url = "https://files.pythonhosted.org/packages/b6/3d/f20f8b886b254e3ad10e15cd4186d3aed49f3e6a35ab37aab9f8f25f7c03/cffi-2.1.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:bf01d8c84cbea96b944c73b22182e6c7c432b3475632b8111dbfdc95ddad6e13", size = 211652, upload-time = "2026-07-06T21:33:40.851Z" }, + { url = "https://files.pythonhosted.org/packages/28/3b/fad54de07260b93ddeef4b96d0131d57ea900675df1d410ae1deee52d7a6/cffi-2.1.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:33eb1ad83ebe8f313e0df035c406227d55a79456704a863fad9842136af5ad7d", size = 210755, upload-time = "2026-07-06T21:33:42.183Z" }, + { url = "https://files.pythonhosted.org/packages/cc/82/3d5c705acb7abbba9bbd7d79b8e62e0f25b6120eb7ae6ac49f1b721722fe/cffi-2.1.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ac0f1a2d0cfa7eea3f2aaf006ab6e70e8feeb16b75d65b7e5939982ca2f11056", size = 223933, upload-time = "2026-07-06T21:33:43.603Z" }, + { url = "https://files.pythonhosted.org/packages/6c/d0/47e338384ab6b1004241002fa616301020cea4fc95f283506565d252f276/cffi-2.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c16914df9fb7f500e440e6875fa23ff5e0b31db01fa9c06af98d59a91f0dc2e4", size = 226749, upload-time = "2026-07-06T21:33:45.046Z" }, + { url = "https://files.pythonhosted.org/packages/70/25/65bd5b58ea4bfdfc15cde02cb5365f89ef8ab8b2adfb8fe5c4bd4233382f/cffi-2.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5ecbd0499275d57506d397eebe1981cee87b47fcd9ef5c22cab7ed7644a39a94", size = 225703, upload-time = "2026-07-06T21:33:46.374Z" }, + { url = "https://files.pythonhosted.org/packages/dc/78/aa01ac599a8a4322533d45a1f9bc93b338276d2d59dabbe7c6d92a775c81/cffi-2.1.0-cp314-cp314t-win32.whl", hash = "sha256:7d034dcffa09e9a46c93fa3a3be402096cb5354ac6e41ab8e5cc9cd8b642ad76", size = 182857, upload-time = "2026-07-06T21:33:47.696Z" }, + { url = "https://files.pythonhosted.org/packages/b9/26/d00496b22de4d4228f32dde94ad996f350c8aad676d63bcca0743c8dea4d/cffi-2.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0582a58f3051372229ca8e7f5f589f9e5632678208d8636fea3676711fdf7fe5", size = 194065, upload-time = "2026-07-06T21:33:48.953Z" }, + { url = "https://files.pythonhosted.org/packages/d5/dd/0c7dbf815a579ff005008a2d815a55d6bb047c349eef536d9dc53d3f0a8d/cffi-2.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:510aeeeac94811b138077451da1fb18b308a5feab47dd2b603af55804155e1c8", size = 186404, upload-time = "2026-07-06T21:33:50.309Z" }, + { url = "https://files.pythonhosted.org/packages/55/c7/8c8c50cb11c6750051daf12164098a9a6f027ac4356967fd4d800a07f242/cffi-2.1.0-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:2e9dabb9abcb7ad15938c7196ad5c1718a4e6d33cc79b4c0209bdb64c4a54a5c", size = 194121, upload-time = "2026-07-06T21:33:56.109Z" }, + { url = "https://files.pythonhosted.org/packages/99/e2/67680bf19a6b60d2bb7ff83baefa2a4c3d2d7dc0f3277034b802e1fc504c/cffi-2.1.0-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:37f525a7e7e50c017fdebe58b787be310ad59357ae43a053943a6e1a6c526001", size = 196820, upload-time = "2026-07-06T21:33:57.288Z" }, + { url = "https://files.pythonhosted.org/packages/ed/da/4bbe583a3b3a5c8c60892124fe17f3fa3656523faf0d3484eae90f091853/cffi-2.1.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:95f2954c2c9473d892eca6e0409f3568b37ab62a8eedb122461f73cc273476e3", size = 184936, upload-time = "2026-07-06T21:33:58.765Z" }, + { url = "https://files.pythonhosted.org/packages/e5/4b/1f4c36ab273980d7aa75bb126ea4f8971f24a96108acad3a0a084028c57b/cffi-2.1.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:cdf2448aab5f661c9315308ec8b93f4e8a1a67a3c733f8631067a2b67d5913dc", size = 185045, upload-time = "2026-07-06T21:34:00.085Z" }, + { url = "https://files.pythonhosted.org/packages/ef/c3/ad299dc38f3583f8d916b299f028af418a9ec98bc695fcbebeae7420691c/cffi-2.1.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:90bec57cf82089383bd06a605b3eb8daebf7e5a668520beaf6e327a83a947699", size = 222342, upload-time = "2026-07-06T21:34:01.814Z" }, + { url = "https://files.pythonhosted.org/packages/eb/d8/df4543cc087245044ed02ef3ad8e0a26619d0075ac7a77a12dc81177851b/cffi-2.1.0-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6274dcb2d15cef48daa73ed1be5a40d501d74dccd0cd6db364776d12cb6ba022", size = 210073, upload-time = "2026-07-06T21:34:03.255Z" }, + { url = "https://files.pythonhosted.org/packages/2c/0e/fac738d73728c6cea2a88a2883dca54892496cbba88a1dc1f2909cb8a6f5/cffi-2.1.0-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:2b71d409cccee78310ab5dec549aed052aaea483346e282c7b02362596e01bb0", size = 208551, upload-time = "2026-07-06T21:34:04.433Z" }, + { url = "https://files.pythonhosted.org/packages/e6/3f/0b04a700dd64f465c93020253a793a82c9b4dff9961f48facd0df945d9b8/cffi-2.1.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7d3538f9c0e50670f4deb93dbb696576e60590369cae2faf7de681e597a8a1f1", size = 221649, upload-time = "2026-07-06T21:34:06.157Z" }, + { url = "https://files.pythonhosted.org/packages/5d/7c/b7379a5704c79eda57ce075869ba70a0368d1c850f803b3c0d078d39dcaf/cffi-2.1.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:8f9ec95b8a043d3dfbc74d9abc6f7baf524dd27a8dc160b0a32ff9cdab650c28", size = 225203, upload-time = "2026-07-06T21:34:07.489Z" }, + { url = "https://files.pythonhosted.org/packages/5a/02/d5e6c43ea85c41bda2a184a3418f195fe7cf602967a8d2b94e085b83deef/cffi-2.1.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:af5e2915d41fe6c961694d7bfdc8562942638200f3ce2765dfb8b745cf997629", size = 223263, upload-time = "2026-07-06T21:34:08.712Z" }, + { url = "https://files.pythonhosted.org/packages/2c/d8/772b8259bf75749adffb1c546828978381fb516f60cf701f6c83daf60c85/cffi-2.1.0-cp315-cp315-win32.whl", hash = "sha256:0a42c688d19fca6e095a53c6a6e2295a5b050a8b289f109adab02a9e61a25de6", size = 177696, upload-time = "2026-07-06T21:34:26.355Z" }, + { url = "https://files.pythonhosted.org/packages/2f/dd/afa2191fc6d57fedd26e5844a2fe2fcc0bbfa00961bbaa5a41e4921e7cca/cffi-2.1.0-cp315-cp315-win_amd64.whl", hash = "sha256:bccbbb5ee76a61f9d99b5bf3846a51d7fca4b6a732fe46f89295610edaf41853", size = 187914, upload-time = "2026-07-06T21:34:27.58Z" }, + { url = "https://files.pythonhosted.org/packages/05/ef/6cd4f8c671517162379dc79cfae5aea9106bc38abb89628d5c16adf6a838/cffi-2.1.0-cp315-cp315-win_arm64.whl", hash = "sha256:8d35c139744adb3e727cd51b1a18324bbe44b8bd41bf8322bca4d41289f48eda", size = 183004, upload-time = "2026-07-06T21:34:28.905Z" }, + { url = "https://files.pythonhosted.org/packages/11/b6/12fc55092817a5faa26fb8c40c7f9d662e11a46ee248c137aafc42517d92/cffi-2.1.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:f9912624a0c0b834b7520d7769b3644453aabc0a7e1c839da7359f050750e9bc", size = 188378, upload-time = "2026-07-06T21:34:09.926Z" }, + { url = "https://files.pythonhosted.org/packages/8d/2e/cdac88979f295fde5daa69622c7d2111e56e7ceb94f211357fbe452339e4/cffi-2.1.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:df92f2aba50eb4d96718b68ef76f2e57a57b54f2fa62333496d16c6d585a85ca", size = 188319, upload-time = "2026-07-06T21:34:11.101Z" }, + { url = "https://files.pythonhosted.org/packages/e0/27/1d0b408497e41a74795af122d7b603c418c5fed0171450f899afd04e594f/cffi-2.1.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0520e1f4c35f44e209cbbb421b67eec42e6a157f59444dfb6058874ff3610e5d", size = 223904, upload-time = "2026-07-06T21:34:12.606Z" }, + { url = "https://files.pythonhosted.org/packages/8b/31/e115c985105dd7ffb32444505f18ceb874bb42d992af05d5dced7ecf1980/cffi-2.1.0-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:3681e031db29958a7502f5c0c9d6bbc4c36cb20f7b104086fa642d1799631ff8", size = 211554, upload-time = "2026-07-06T21:34:13.987Z" }, + { url = "https://files.pythonhosted.org/packages/5a/67/9e6e09409336d9e515c58367e7cfcf4f89df06ad25252675595a58eb59d5/cffi-2.1.0-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:762f99479dcb369f60ab9017ad4ab97a36a1dd7c1ee5a3b15db0f4b8659120cd", size = 210795, upload-time = "2026-07-06T21:34:15.972Z" }, + { url = "https://files.pythonhosted.org/packages/19/e5/d3cc82a4a0be7902af279c04181ad038449c096734464a5ae1de3e1401bd/cffi-2.1.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0611e7ebf90573a535ebdc33ae9da222d037853983e13359f580fab781ca017f", size = 223843, upload-time = "2026-07-06T21:34:17.509Z" }, + { url = "https://files.pythonhosted.org/packages/b9/65/b434abc97ce7cecc2c640fde160507c0ecc7e21544b483ba3325d2e2ea17/cffi-2.1.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:86cf8755a791f72c85dc287128cc62d4f24d392e3f1e15837245623f4a33cccc", size = 226773, upload-time = "2026-07-06T21:34:19.05Z" }, + { url = "https://files.pythonhosted.org/packages/b5/9f/d4dc66ca651eb1145a133314cda721abf13cfac3d28c4a0402263ae6ad75/cffi-2.1.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:ba00f661f8ba35d075c937174e27c2c421cec3942fd2e0ea3e66996757c0fdd9", size = 225719, upload-time = "2026-07-06T21:34:20.576Z" }, + { url = "https://files.pythonhosted.org/packages/68/5a/e536c528bc8057496c360c0978559a2dc45653f89dd6151078aa7d8fca1a/cffi-2.1.0-cp315-cp315t-win32.whl", hash = "sha256:cb96698e3c7413d906ce83f8ffd245ec1bd94707541f299d0ce4d6b0193e982b", size = 182760, upload-time = "2026-07-06T21:34:22.059Z" }, + { url = "https://files.pythonhosted.org/packages/d3/0b/0ffe8b82d3875bced5fa1e7986a7a46b748262a40ab7f60b475eb9fb1bb3/cffi-2.1.0-cp315-cp315t-win_amd64.whl", hash = "sha256:f146d154428a2523f9cc7936c02353c2459b8f6cf07d3cd1ee1c0a611109c5d5", size = 193769, upload-time = "2026-07-06T21:34:23.589Z" }, + { url = "https://files.pythonhosted.org/packages/a0/17/1073b53b68c9b5ca6914adf5f8bf55aacc2d3be102418c90700160ea8605/cffi-2.1.0-cp315-cp315t-win_arm64.whl", hash = "sha256:cbb7640ce37159548d2147b5b8c241f962143d4c71231431820783f4dc78f210", size = 186405, upload-time = "2026-07-06T21:34:24.857Z" }, ] [[package]] name = "charset-normalizer" -version = "3.4.6" +version = "3.4.9" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7b/60/e3bec1881450851b087e301bedc3daa9377a4d45f1c26aa90b0b235e38aa/charset_normalizer-3.4.6.tar.gz", hash = "sha256:1ae6b62897110aa7c79ea2f5dd38d1abca6db663687c0b1ad9aed6f6bae3d9d6", size = 143363, upload-time = "2026-03-15T18:53:25.478Z" } +sdist = { url = "https://files.pythonhosted.org/packages/bd/2a/23f34ec9d04624958e137efdc394888716353190e75f25dd22c7a2c7a8aa/charset_normalizer-3.4.9.tar.gz", hash = "sha256:673611bbd43f0810bec0b0f028ddeaaa501190339cac411f347ac76917c3ae7b", size = 152439, upload-time = "2026-07-07T14:34:58.454Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e6/8c/2c56124c6dc53a774d435f985b5973bc592f42d437be58c0c92d65ae7296/charset_normalizer-3.4.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:2e1d8ca8611099001949d1cdfaefc510cf0f212484fe7c565f735b68c78c3c95", size = 298751, upload-time = "2026-03-15T18:50:00.003Z" }, - { url = "https://files.pythonhosted.org/packages/86/2a/2a7db6b314b966a3bcad8c731c0719c60b931b931de7ae9f34b2839289ee/charset_normalizer-3.4.6-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e25369dc110d58ddf29b949377a93e0716d72a24f62bad72b2b39f155949c1fd", size = 200027, upload-time = "2026-03-15T18:50:01.702Z" }, - { url = "https://files.pythonhosted.org/packages/68/f2/0fe775c74ae25e2a3b07b01538fc162737b3e3f795bada3bc26f4d4d495c/charset_normalizer-3.4.6-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:259695e2ccc253feb2a016303543d691825e920917e31f894ca1a687982b1de4", size = 220741, upload-time = "2026-03-15T18:50:03.194Z" }, - { url = "https://files.pythonhosted.org/packages/10/98/8085596e41f00b27dd6aa1e68413d1ddda7e605f34dd546833c61fddd709/charset_normalizer-3.4.6-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:dda86aba335c902b6149a02a55b38e96287157e609200811837678214ba2b1db", size = 215802, upload-time = "2026-03-15T18:50:05.859Z" }, - { url = "https://files.pythonhosted.org/packages/fd/ce/865e4e09b041bad659d682bbd98b47fb490b8e124f9398c9448065f64fee/charset_normalizer-3.4.6-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:51fb3c322c81d20567019778cb5a4a6f2dc1c200b886bc0d636238e364848c89", size = 207908, upload-time = "2026-03-15T18:50:07.676Z" }, - { url = "https://files.pythonhosted.org/packages/a8/54/8c757f1f7349262898c2f169e0d562b39dcb977503f18fdf0814e923db78/charset_normalizer-3.4.6-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:4482481cb0572180b6fd976a4d5c72a30263e98564da68b86ec91f0fe35e8565", size = 194357, upload-time = "2026-03-15T18:50:09.327Z" }, - { url = "https://files.pythonhosted.org/packages/6f/29/e88f2fac9218907fc7a70722b393d1bbe8334c61fe9c46640dba349b6e66/charset_normalizer-3.4.6-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:39f5068d35621da2881271e5c3205125cc456f54e9030d3f723288c873a71bf9", size = 205610, upload-time = "2026-03-15T18:50:10.732Z" }, - { url = "https://files.pythonhosted.org/packages/4c/c5/21d7bb0cb415287178450171d130bed9d664211fdd59731ed2c34267b07d/charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8bea55c4eef25b0b19a0337dc4e3f9a15b00d569c77211fa8cde38684f234fb7", size = 203512, upload-time = "2026-03-15T18:50:12.535Z" }, - { url = "https://files.pythonhosted.org/packages/a4/be/ce52f3c7fdb35cc987ad38a53ebcef52eec498f4fb6c66ecfe62cfe57ba2/charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:f0cdaecd4c953bfae0b6bb64910aaaca5a424ad9c72d85cb88417bb9814f7550", size = 195398, upload-time = "2026-03-15T18:50:14.236Z" }, - { url = "https://files.pythonhosted.org/packages/81/a0/3ab5dd39d4859a3555e5dadfc8a9fa7f8352f8c183d1a65c90264517da0e/charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:150b8ce8e830eb7ccb029ec9ca36022f756986aaaa7956aad6d9ec90089338c0", size = 221772, upload-time = "2026-03-15T18:50:15.581Z" }, - { url = "https://files.pythonhosted.org/packages/04/6e/6a4e41a97ba6b2fa87f849c41e4d229449a586be85053c4d90135fe82d26/charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:e68c14b04827dd76dcbd1aeea9e604e3e4b78322d8faf2f8132c7138efa340a8", size = 205759, upload-time = "2026-03-15T18:50:17.047Z" }, - { url = "https://files.pythonhosted.org/packages/db/3b/34a712a5ee64a6957bf355b01dc17b12de457638d436fdb05d01e463cd1c/charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:3778fd7d7cd04ae8f54651f4a7a0bd6e39a0cf20f801720a4c21d80e9b7ad6b0", size = 216938, upload-time = "2026-03-15T18:50:18.44Z" }, - { url = "https://files.pythonhosted.org/packages/cb/05/5bd1e12da9ab18790af05c61aafd01a60f489778179b621ac2a305243c62/charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:dad6e0f2e481fffdcf776d10ebee25e0ef89f16d691f1e5dee4b586375fdc64b", size = 210138, upload-time = "2026-03-15T18:50:19.852Z" }, - { url = "https://files.pythonhosted.org/packages/bd/8e/3cb9e2d998ff6b21c0a1860343cb7b83eba9cdb66b91410e18fc4969d6ab/charset_normalizer-3.4.6-cp310-cp310-win32.whl", hash = "sha256:74a2e659c7ecbc73562e2a15e05039f1e22c75b7c7618b4b574a3ea9118d1557", size = 144137, upload-time = "2026-03-15T18:50:21.505Z" }, - { url = "https://files.pythonhosted.org/packages/d8/8f/78f5489ffadb0db3eb7aff53d31c24531d33eb545f0c6f6567c25f49a5ff/charset_normalizer-3.4.6-cp310-cp310-win_amd64.whl", hash = "sha256:aa9cccf4a44b9b62d8ba8b4dd06c649ba683e4bf04eea606d2e94cfc2d6ff4d6", size = 154244, upload-time = "2026-03-15T18:50:22.81Z" }, - { url = "https://files.pythonhosted.org/packages/e4/74/e472659dffb0cadb2f411282d2d76c60da1fc94076d7fffed4ae8a93ec01/charset_normalizer-3.4.6-cp310-cp310-win_arm64.whl", hash = "sha256:e985a16ff513596f217cee86c21371b8cd011c0f6f056d0920aa2d926c544058", size = 143312, upload-time = "2026-03-15T18:50:24.074Z" }, - { url = "https://files.pythonhosted.org/packages/62/28/ff6f234e628a2de61c458be2779cb182bc03f6eec12200d4a525bbfc9741/charset_normalizer-3.4.6-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:82060f995ab5003a2d6e0f4ad29065b7672b6593c8c63559beefe5b443242c3e", size = 293582, upload-time = "2026-03-15T18:50:25.454Z" }, - { url = "https://files.pythonhosted.org/packages/1c/b7/b1a117e5385cbdb3205f6055403c2a2a220c5ea80b8716c324eaf75c5c95/charset_normalizer-3.4.6-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:60c74963d8350241a79cb8feea80e54d518f72c26db618862a8f53e5023deaf9", size = 197240, upload-time = "2026-03-15T18:50:27.196Z" }, - { url = "https://files.pythonhosted.org/packages/a1/5f/2574f0f09f3c3bc1b2f992e20bce6546cb1f17e111c5be07308dc5427956/charset_normalizer-3.4.6-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f6e4333fb15c83f7d1482a76d45a0818897b3d33f00efd215528ff7c51b8e35d", size = 217363, upload-time = "2026-03-15T18:50:28.601Z" }, - { url = "https://files.pythonhosted.org/packages/4a/d1/0ae20ad77bc949ddd39b51bf383b6ca932f2916074c95cad34ae465ab71f/charset_normalizer-3.4.6-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bc72863f4d9aba2e8fd9085e63548a324ba706d2ea2c83b260da08a59b9482de", size = 212994, upload-time = "2026-03-15T18:50:30.102Z" }, - { url = "https://files.pythonhosted.org/packages/60/ac/3233d262a310c1b12633536a07cde5ddd16985e6e7e238e9f3f9423d8eb9/charset_normalizer-3.4.6-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9cc4fc6c196d6a8b76629a70ddfcd4635a6898756e2d9cac5565cf0654605d73", size = 204697, upload-time = "2026-03-15T18:50:31.654Z" }, - { url = "https://files.pythonhosted.org/packages/25/3c/8a18fc411f085b82303cfb7154eed5bd49c77035eb7608d049468b53f87c/charset_normalizer-3.4.6-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:0c173ce3a681f309f31b87125fecec7a5d1347261ea11ebbb856fa6006b23c8c", size = 191673, upload-time = "2026-03-15T18:50:33.433Z" }, - { url = "https://files.pythonhosted.org/packages/ff/a7/11cfe61d6c5c5c7438d6ba40919d0306ed83c9ab957f3d4da2277ff67836/charset_normalizer-3.4.6-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c907cdc8109f6c619e6254212e794d6548373cc40e1ec75e6e3823d9135d29cc", size = 201120, upload-time = "2026-03-15T18:50:35.105Z" }, - { url = "https://files.pythonhosted.org/packages/b5/10/cf491fa1abd47c02f69687046b896c950b92b6cd7337a27e6548adbec8e4/charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:404a1e552cf5b675a87f0651f8b79f5f1e6fd100ee88dc612f89aa16abd4486f", size = 200911, upload-time = "2026-03-15T18:50:36.819Z" }, - { url = "https://files.pythonhosted.org/packages/28/70/039796160b48b18ed466fde0af84c1b090c4e288fae26cd674ad04a2d703/charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:e3c701e954abf6fc03a49f7c579cc80c2c6cc52525340ca3186c41d3f33482ef", size = 192516, upload-time = "2026-03-15T18:50:38.228Z" }, - { url = "https://files.pythonhosted.org/packages/ff/34/c56f3223393d6ff3124b9e78f7de738047c2d6bc40a4f16ac0c9d7a1cb3c/charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:7a6967aaf043bceabab5412ed6bd6bd26603dae84d5cb75bf8d9a74a4959d398", size = 218795, upload-time = "2026-03-15T18:50:39.664Z" }, - { url = "https://files.pythonhosted.org/packages/e8/3b/ce2d4f86c5282191a041fdc5a4ce18f1c6bd40a5bd1f74cf8625f08d51c1/charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:5feb91325bbceade6afab43eb3b508c63ee53579fe896c77137ded51c6b6958e", size = 201833, upload-time = "2026-03-15T18:50:41.552Z" }, - { url = "https://files.pythonhosted.org/packages/3b/9b/b6a9f76b0fd7c5b5ec58b228ff7e85095370282150f0bd50b3126f5506d6/charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:f820f24b09e3e779fe84c3c456cb4108a7aa639b0d1f02c28046e11bfcd088ed", size = 213920, upload-time = "2026-03-15T18:50:43.33Z" }, - { url = "https://files.pythonhosted.org/packages/ae/98/7bc23513a33d8172365ed30ee3a3b3fe1ece14a395e5fc94129541fc6003/charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b35b200d6a71b9839a46b9b7fff66b6638bb52fc9658aa58796b0326595d3021", size = 206951, upload-time = "2026-03-15T18:50:44.789Z" }, - { url = "https://files.pythonhosted.org/packages/32/73/c0b86f3d1458468e11aec870e6b3feac931facbe105a894b552b0e518e79/charset_normalizer-3.4.6-cp311-cp311-win32.whl", hash = "sha256:9ca4c0b502ab399ef89248a2c84c54954f77a070f28e546a85e91da627d1301e", size = 143703, upload-time = "2026-03-15T18:50:46.103Z" }, - { url = "https://files.pythonhosted.org/packages/c6/e3/76f2facfe8eddee0bbd38d2594e709033338eae44ebf1738bcefe0a06185/charset_normalizer-3.4.6-cp311-cp311-win_amd64.whl", hash = "sha256:a9e68c9d88823b274cf1e72f28cb5dc89c990edf430b0bfd3e2fb0785bfeabf4", size = 153857, upload-time = "2026-03-15T18:50:47.563Z" }, - { url = "https://files.pythonhosted.org/packages/e2/dc/9abe19c9b27e6cd3636036b9d1b387b78c40dedbf0b47f9366737684b4b0/charset_normalizer-3.4.6-cp311-cp311-win_arm64.whl", hash = "sha256:97d0235baafca5f2b09cf332cc275f021e694e8362c6bb9c96fc9a0eb74fc316", size = 142751, upload-time = "2026-03-15T18:50:49.234Z" }, - { url = "https://files.pythonhosted.org/packages/e5/62/c0815c992c9545347aeea7859b50dc9044d147e2e7278329c6e02ac9a616/charset_normalizer-3.4.6-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:2ef7fedc7a6ecbe99969cd09632516738a97eeb8bd7258bf8a0f23114c057dab", size = 295154, upload-time = "2026-03-15T18:50:50.88Z" }, - { url = "https://files.pythonhosted.org/packages/a8/37/bdca6613c2e3c58c7421891d80cc3efa1d32e882f7c4a7ee6039c3fc951a/charset_normalizer-3.4.6-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a4ea868bc28109052790eb2b52a9ab33f3aa7adc02f96673526ff47419490e21", size = 199191, upload-time = "2026-03-15T18:50:52.658Z" }, - { url = "https://files.pythonhosted.org/packages/6c/92/9934d1bbd69f7f398b38c5dae1cbf9cc672e7c34a4adf7b17c0a9c17d15d/charset_normalizer-3.4.6-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:836ab36280f21fc1a03c99cd05c6b7af70d2697e374c7af0b61ed271401a72a2", size = 218674, upload-time = "2026-03-15T18:50:54.102Z" }, - { url = "https://files.pythonhosted.org/packages/af/90/25f6ab406659286be929fd89ab0e78e38aa183fc374e03aa3c12d730af8a/charset_normalizer-3.4.6-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f1ce721c8a7dfec21fcbdfe04e8f68174183cf4e8188e0645e92aa23985c57ff", size = 215259, upload-time = "2026-03-15T18:50:55.616Z" }, - { url = "https://files.pythonhosted.org/packages/4e/ef/79a463eb0fff7f96afa04c1d4c51f8fc85426f918db467854bfb6a569ce3/charset_normalizer-3.4.6-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e28d62a8fc7a1fa411c43bd65e346f3bce9716dc51b897fbe930c5987b402d5", size = 207276, upload-time = "2026-03-15T18:50:57.054Z" }, - { url = "https://files.pythonhosted.org/packages/f7/72/d0426afec4b71dc159fa6b4e68f868cd5a3ecd918fec5813a15d292a7d10/charset_normalizer-3.4.6-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:530d548084c4a9f7a16ed4a294d459b4f229db50df689bfe92027452452943a0", size = 195161, upload-time = "2026-03-15T18:50:58.686Z" }, - { url = "https://files.pythonhosted.org/packages/bf/18/c82b06a68bfcb6ce55e508225d210c7e6a4ea122bfc0748892f3dc4e8e11/charset_normalizer-3.4.6-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:30f445ae60aad5e1f8bdbb3108e39f6fbc09f4ea16c815c66578878325f8f15a", size = 203452, upload-time = "2026-03-15T18:51:00.196Z" }, - { url = "https://files.pythonhosted.org/packages/44/d6/0c25979b92f8adafdbb946160348d8d44aa60ce99afdc27df524379875cb/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ac2393c73378fea4e52aa56285a3d64be50f1a12395afef9cce47772f60334c2", size = 202272, upload-time = "2026-03-15T18:51:01.703Z" }, - { url = "https://files.pythonhosted.org/packages/2e/3d/7fea3e8fe84136bebbac715dd1221cc25c173c57a699c030ab9b8900cbb7/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:90ca27cd8da8118b18a52d5f547859cc1f8354a00cd1e8e5120df3e30d6279e5", size = 195622, upload-time = "2026-03-15T18:51:03.526Z" }, - { url = "https://files.pythonhosted.org/packages/57/8a/d6f7fd5cb96c58ef2f681424fbca01264461336d2a7fc875e4446b1f1346/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8e5a94886bedca0f9b78fecd6afb6629142fd2605aa70a125d49f4edc6037ee6", size = 220056, upload-time = "2026-03-15T18:51:05.269Z" }, - { url = "https://files.pythonhosted.org/packages/16/50/478cdda782c8c9c3fb5da3cc72dd7f331f031e7f1363a893cdd6ca0f8de0/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:695f5c2823691a25f17bc5d5ffe79fa90972cc34b002ac6c843bb8a1720e950d", size = 203751, upload-time = "2026-03-15T18:51:06.858Z" }, - { url = "https://files.pythonhosted.org/packages/75/fc/cc2fcac943939c8e4d8791abfa139f685e5150cae9f94b60f12520feaa9b/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:231d4da14bcd9301310faf492051bee27df11f2bc7549bc0bb41fef11b82daa2", size = 216563, upload-time = "2026-03-15T18:51:08.564Z" }, - { url = "https://files.pythonhosted.org/packages/a8/b7/a4add1d9a5f68f3d037261aecca83abdb0ab15960a3591d340e829b37298/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a056d1ad2633548ca18ffa2f85c202cfb48b68615129143915b8dc72a806a923", size = 209265, upload-time = "2026-03-15T18:51:10.312Z" }, - { url = "https://files.pythonhosted.org/packages/6c/18/c094561b5d64a24277707698e54b7f67bd17a4f857bbfbb1072bba07c8bf/charset_normalizer-3.4.6-cp312-cp312-win32.whl", hash = "sha256:c2274ca724536f173122f36c98ce188fd24ce3dad886ec2b7af859518ce008a4", size = 144229, upload-time = "2026-03-15T18:51:11.694Z" }, - { url = "https://files.pythonhosted.org/packages/ab/20/0567efb3a8fd481b8f34f739ebddc098ed062a59fed41a8d193a61939e8f/charset_normalizer-3.4.6-cp312-cp312-win_amd64.whl", hash = "sha256:c8ae56368f8cc97c7e40a7ee18e1cedaf8e780cd8bc5ed5ac8b81f238614facb", size = 154277, upload-time = "2026-03-15T18:51:13.004Z" }, - { url = "https://files.pythonhosted.org/packages/15/57/28d79b44b51933119e21f65479d0864a8d5893e494cf5daab15df0247c17/charset_normalizer-3.4.6-cp312-cp312-win_arm64.whl", hash = "sha256:899d28f422116b08be5118ef350c292b36fc15ec2daeb9ea987c89281c7bb5c4", size = 142817, upload-time = "2026-03-15T18:51:14.408Z" }, - { url = "https://files.pythonhosted.org/packages/1e/1d/4fdabeef4e231153b6ed7567602f3b68265ec4e5b76d6024cf647d43d981/charset_normalizer-3.4.6-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:11afb56037cbc4b1555a34dd69151e8e069bee82e613a73bef6e714ce733585f", size = 294823, upload-time = "2026-03-15T18:51:15.755Z" }, - { url = "https://files.pythonhosted.org/packages/47/7b/20e809b89c69d37be748d98e84dce6820bf663cf19cf6b942c951a3e8f41/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:423fb7e748a08f854a08a222b983f4df1912b1daedce51a72bd24fe8f26a1843", size = 198527, upload-time = "2026-03-15T18:51:17.177Z" }, - { url = "https://files.pythonhosted.org/packages/37/a6/4f8d27527d59c039dce6f7622593cdcd3d70a8504d87d09eb11e9fdc6062/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d73beaac5e90173ac3deb9928a74763a6d230f494e4bfb422c217a0ad8e629bf", size = 218388, upload-time = "2026-03-15T18:51:18.934Z" }, - { url = "https://files.pythonhosted.org/packages/f6/9b/4770ccb3e491a9bacf1c46cc8b812214fe367c86a96353ccc6daf87b01ec/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d60377dce4511655582e300dc1e5a5f24ba0cb229005a1d5c8d0cb72bb758ab8", size = 214563, upload-time = "2026-03-15T18:51:20.374Z" }, - { url = "https://files.pythonhosted.org/packages/2b/58/a199d245894b12db0b957d627516c78e055adc3a0d978bc7f65ddaf7c399/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:530e8cebeea0d76bdcf93357aa5e41336f48c3dc709ac52da2bb167c5b8271d9", size = 206587, upload-time = "2026-03-15T18:51:21.807Z" }, - { url = "https://files.pythonhosted.org/packages/7e/70/3def227f1ec56f5c69dfc8392b8bd63b11a18ca8178d9211d7cc5e5e4f27/charset_normalizer-3.4.6-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:a26611d9987b230566f24a0a125f17fe0de6a6aff9f25c9f564aaa2721a5fb88", size = 194724, upload-time = "2026-03-15T18:51:23.508Z" }, - { url = "https://files.pythonhosted.org/packages/58/ab/9318352e220c05efd31c2779a23b50969dc94b985a2efa643ed9077bfca5/charset_normalizer-3.4.6-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:34315ff4fc374b285ad7f4a0bf7dcbfe769e1b104230d40f49f700d4ab6bbd84", size = 202956, upload-time = "2026-03-15T18:51:25.239Z" }, - { url = "https://files.pythonhosted.org/packages/75/13/f3550a3ac25b70f87ac98c40d3199a8503676c2f1620efbf8d42095cfc40/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5f8ddd609f9e1af8c7bd6e2aca279c931aefecd148a14402d4e368f3171769fd", size = 201923, upload-time = "2026-03-15T18:51:26.682Z" }, - { url = "https://files.pythonhosted.org/packages/1b/db/c5c643b912740b45e8eec21de1bbab8e7fc085944d37e1e709d3dcd9d72f/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:80d0a5615143c0b3225e5e3ef22c8d5d51f3f72ce0ea6fb84c943546c7b25b6c", size = 195366, upload-time = "2026-03-15T18:51:28.129Z" }, - { url = "https://files.pythonhosted.org/packages/5a/67/3b1c62744f9b2448443e0eb160d8b001c849ec3fef591e012eda6484787c/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:92734d4d8d187a354a556626c221cd1a892a4e0802ccb2af432a1d85ec012194", size = 219752, upload-time = "2026-03-15T18:51:29.556Z" }, - { url = "https://files.pythonhosted.org/packages/f6/98/32ffbaf7f0366ffb0445930b87d103f6b406bc2c271563644bde8a2b1093/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:613f19aa6e082cf96e17e3ffd89383343d0d589abda756b7764cf78361fd41dc", size = 203296, upload-time = "2026-03-15T18:51:30.921Z" }, - { url = "https://files.pythonhosted.org/packages/41/12/5d308c1bbe60cabb0c5ef511574a647067e2a1f631bc8634fcafaccd8293/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:2b1a63e8224e401cafe7739f77efd3f9e7f5f2026bda4aead8e59afab537784f", size = 215956, upload-time = "2026-03-15T18:51:32.399Z" }, - { url = "https://files.pythonhosted.org/packages/53/e9/5f85f6c5e20669dbe56b165c67b0260547dea97dba7e187938833d791687/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6cceb5473417d28edd20c6c984ab6fee6c6267d38d906823ebfe20b03d607dc2", size = 208652, upload-time = "2026-03-15T18:51:34.214Z" }, - { url = "https://files.pythonhosted.org/packages/f1/11/897052ea6af56df3eef3ca94edafee410ca699ca0c7b87960ad19932c55e/charset_normalizer-3.4.6-cp313-cp313-win32.whl", hash = "sha256:d7de2637729c67d67cf87614b566626057e95c303bc0a55ffe391f5205e7003d", size = 143940, upload-time = "2026-03-15T18:51:36.15Z" }, - { url = "https://files.pythonhosted.org/packages/a1/5c/724b6b363603e419829f561c854b87ed7c7e31231a7908708ac086cdf3e2/charset_normalizer-3.4.6-cp313-cp313-win_amd64.whl", hash = "sha256:572d7c822caf521f0525ba1bce1a622a0b85cf47ffbdae6c9c19e3b5ac3c4389", size = 154101, upload-time = "2026-03-15T18:51:37.876Z" }, - { url = "https://files.pythonhosted.org/packages/01/a5/7abf15b4c0968e47020f9ca0935fb3274deb87cb288cd187cad92e8cdffd/charset_normalizer-3.4.6-cp313-cp313-win_arm64.whl", hash = "sha256:a4474d924a47185a06411e0064b803c68be044be2d60e50e8bddcc2649957c1f", size = 143109, upload-time = "2026-03-15T18:51:39.565Z" }, - { url = "https://files.pythonhosted.org/packages/25/6f/ffe1e1259f384594063ea1869bfb6be5cdb8bc81020fc36c3636bc8302a1/charset_normalizer-3.4.6-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:9cc6e6d9e571d2f863fa77700701dae73ed5f78881efc8b3f9a4398772ff53e8", size = 294458, upload-time = "2026-03-15T18:51:41.134Z" }, - { url = "https://files.pythonhosted.org/packages/56/60/09bb6c13a8c1016c2ed5c6a6488e4ffef506461aa5161662bd7636936fb1/charset_normalizer-3.4.6-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef5960d965e67165d75b7c7ffc60a83ec5abfc5c11b764ec13ea54fbef8b4421", size = 199277, upload-time = "2026-03-15T18:51:42.953Z" }, - { url = "https://files.pythonhosted.org/packages/00/50/dcfbb72a5138bbefdc3332e8d81a23494bf67998b4b100703fd15fa52d81/charset_normalizer-3.4.6-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b3694e3f87f8ac7ce279d4355645b3c878d24d1424581b46282f24b92f5a4ae2", size = 218758, upload-time = "2026-03-15T18:51:44.339Z" }, - { url = "https://files.pythonhosted.org/packages/03/b3/d79a9a191bb75f5aa81f3aaaa387ef29ce7cb7a9e5074ba8ea095cc073c2/charset_normalizer-3.4.6-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5d11595abf8dd942a77883a39d81433739b287b6aa71620f15164f8096221b30", size = 215299, upload-time = "2026-03-15T18:51:45.871Z" }, - { url = "https://files.pythonhosted.org/packages/76/7e/bc8911719f7084f72fd545f647601ea3532363927f807d296a8c88a62c0d/charset_normalizer-3.4.6-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7bda6eebafd42133efdca535b04ccb338ab29467b3f7bf79569883676fc628db", size = 206811, upload-time = "2026-03-15T18:51:47.308Z" }, - { url = "https://files.pythonhosted.org/packages/e2/40/c430b969d41dda0c465aa36cc7c2c068afb67177bef50905ac371b28ccc7/charset_normalizer-3.4.6-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:bbc8c8650c6e51041ad1be191742b8b421d05bbd3410f43fa2a00c8db87678e8", size = 193706, upload-time = "2026-03-15T18:51:48.849Z" }, - { url = "https://files.pythonhosted.org/packages/48/15/e35e0590af254f7df984de1323640ef375df5761f615b6225ba8deb9799a/charset_normalizer-3.4.6-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:22c6f0c2fbc31e76c3b8a86fba1a56eda6166e238c29cdd3d14befdb4a4e4815", size = 202706, upload-time = "2026-03-15T18:51:50.257Z" }, - { url = "https://files.pythonhosted.org/packages/5e/bd/f736f7b9cc5e93a18b794a50346bb16fbfd6b37f99e8f306f7951d27c17c/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7edbed096e4a4798710ed6bc75dcaa2a21b68b6c356553ac4823c3658d53743a", size = 202497, upload-time = "2026-03-15T18:51:52.012Z" }, - { url = "https://files.pythonhosted.org/packages/9d/ba/2cc9e3e7dfdf7760a6ed8da7446d22536f3d0ce114ac63dee2a5a3599e62/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:7f9019c9cb613f084481bd6a100b12e1547cf2efe362d873c2e31e4035a6fa43", size = 193511, upload-time = "2026-03-15T18:51:53.723Z" }, - { url = "https://files.pythonhosted.org/packages/9e/cb/5be49b5f776e5613be07298c80e1b02a2d900f7a7de807230595c85a8b2e/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:58c948d0d086229efc484fe2f30c2d382c86720f55cd9bc33591774348ad44e0", size = 220133, upload-time = "2026-03-15T18:51:55.333Z" }, - { url = "https://files.pythonhosted.org/packages/83/43/99f1b5dad345accb322c80c7821071554f791a95ee50c1c90041c157ae99/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:419a9d91bd238052642a51938af8ac05da5b3343becde08d5cdeab9046df9ee1", size = 203035, upload-time = "2026-03-15T18:51:56.736Z" }, - { url = "https://files.pythonhosted.org/packages/87/9a/62c2cb6a531483b55dddff1a68b3d891a8b498f3ca555fbcf2978e804d9d/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5273b9f0b5835ff0350c0828faea623c68bfa65b792720c453e22b25cc72930f", size = 216321, upload-time = "2026-03-15T18:51:58.17Z" }, - { url = "https://files.pythonhosted.org/packages/6e/79/94a010ff81e3aec7c293eb82c28f930918e517bc144c9906a060844462eb/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:0e901eb1049fdb80f5bd11ed5ea1e498ec423102f7a9b9e4645d5b8204ff2815", size = 208973, upload-time = "2026-03-15T18:51:59.998Z" }, - { url = "https://files.pythonhosted.org/packages/2a/57/4ecff6d4ec8585342f0c71bc03efaa99cb7468f7c91a57b105bcd561cea8/charset_normalizer-3.4.6-cp314-cp314-win32.whl", hash = "sha256:b4ff1d35e8c5bd078be89349b6f3a845128e685e751b6ea1169cf2160b344c4d", size = 144610, upload-time = "2026-03-15T18:52:02.213Z" }, - { url = "https://files.pythonhosted.org/packages/80/94/8434a02d9d7f168c25767c64671fead8d599744a05d6a6c877144c754246/charset_normalizer-3.4.6-cp314-cp314-win_amd64.whl", hash = "sha256:74119174722c4349af9708993118581686f343adc1c8c9c007d59be90d077f3f", size = 154962, upload-time = "2026-03-15T18:52:03.658Z" }, - { url = "https://files.pythonhosted.org/packages/46/4c/48f2cdbfd923026503dfd67ccea45c94fd8fe988d9056b468579c66ed62b/charset_normalizer-3.4.6-cp314-cp314-win_arm64.whl", hash = "sha256:e5bcc1a1ae744e0bb59641171ae53743760130600da8db48cbb6e4918e186e4e", size = 143595, upload-time = "2026-03-15T18:52:05.123Z" }, - { url = "https://files.pythonhosted.org/packages/31/93/8878be7569f87b14f1d52032946131bcb6ebbd8af3e20446bc04053dc3f1/charset_normalizer-3.4.6-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:ad8faf8df23f0378c6d527d8b0b15ea4a2e23c89376877c598c4870d1b2c7866", size = 314828, upload-time = "2026-03-15T18:52:06.831Z" }, - { url = "https://files.pythonhosted.org/packages/06/b6/fae511ca98aac69ecc35cde828b0a3d146325dd03d99655ad38fc2cc3293/charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f5ea69428fa1b49573eef0cc44a1d43bebd45ad0c611eb7d7eac760c7ae771bc", size = 208138, upload-time = "2026-03-15T18:52:08.239Z" }, - { url = "https://files.pythonhosted.org/packages/54/57/64caf6e1bf07274a1e0b7c160a55ee9e8c9ec32c46846ce59b9c333f7008/charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:06a7e86163334edfc5d20fe104db92fcd666e5a5df0977cb5680a506fe26cc8e", size = 224679, upload-time = "2026-03-15T18:52:10.043Z" }, - { url = "https://files.pythonhosted.org/packages/aa/cb/9ff5a25b9273ef160861b41f6937f86fae18b0792fe0a8e75e06acb08f1d/charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e1f6e2f00a6b8edb562826e4632e26d063ac10307e80f7461f7de3ad8ef3f077", size = 223475, upload-time = "2026-03-15T18:52:11.854Z" }, - { url = "https://files.pythonhosted.org/packages/fc/97/440635fc093b8d7347502a377031f9605a1039c958f3cd18dcacffb37743/charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:95b52c68d64c1878818687a473a10547b3292e82b6f6fe483808fb1468e2f52f", size = 215230, upload-time = "2026-03-15T18:52:13.325Z" }, - { url = "https://files.pythonhosted.org/packages/cd/24/afff630feb571a13f07c8539fbb502d2ab494019492aaffc78ef41f1d1d0/charset_normalizer-3.4.6-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:7504e9b7dc05f99a9bbb4525c67a2c155073b44d720470a148b34166a69c054e", size = 199045, upload-time = "2026-03-15T18:52:14.752Z" }, - { url = "https://files.pythonhosted.org/packages/e5/17/d1399ecdaf7e0498c327433e7eefdd862b41236a7e484355b8e0e5ebd64b/charset_normalizer-3.4.6-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:172985e4ff804a7ad08eebec0a1640ece87ba5041d565fff23c8f99c1f389484", size = 211658, upload-time = "2026-03-15T18:52:16.278Z" }, - { url = "https://files.pythonhosted.org/packages/b5/38/16baa0affb957b3d880e5ac2144caf3f9d7de7bc4a91842e447fbb5e8b67/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:4be9f4830ba8741527693848403e2c457c16e499100963ec711b1c6f2049b7c7", size = 210769, upload-time = "2026-03-15T18:52:17.782Z" }, - { url = "https://files.pythonhosted.org/packages/05/34/c531bc6ac4c21da9ddfddb3107be2287188b3ea4b53b70fc58f2a77ac8d8/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:79090741d842f564b1b2827c0b82d846405b744d31e84f18d7a7b41c20e473ff", size = 201328, upload-time = "2026-03-15T18:52:19.553Z" }, - { url = "https://files.pythonhosted.org/packages/fa/73/a5a1e9ca5f234519c1953608a03fe109c306b97fdfb25f09182babad51a7/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:87725cfb1a4f1f8c2fc9890ae2f42094120f4b44db9360be5d99a4c6b0e03a9e", size = 225302, upload-time = "2026-03-15T18:52:21.043Z" }, - { url = "https://files.pythonhosted.org/packages/ba/f6/cd782923d112d296294dea4bcc7af5a7ae0f86ab79f8fefbda5526b6cfc0/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:fcce033e4021347d80ed9c66dcf1e7b1546319834b74445f561d2e2221de5659", size = 211127, upload-time = "2026-03-15T18:52:22.491Z" }, - { url = "https://files.pythonhosted.org/packages/0e/c5/0b6898950627af7d6103a449b22320372c24c6feda91aa24e201a478d161/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:ca0276464d148c72defa8bb4390cce01b4a0e425f3b50d1435aa6d7a18107602", size = 222840, upload-time = "2026-03-15T18:52:24.113Z" }, - { url = "https://files.pythonhosted.org/packages/7d/25/c4bba773bef442cbdc06111d40daa3de5050a676fa26e85090fc54dd12f0/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:197c1a244a274bb016dd8b79204850144ef77fe81c5b797dc389327adb552407", size = 216890, upload-time = "2026-03-15T18:52:25.541Z" }, - { url = "https://files.pythonhosted.org/packages/35/1a/05dacadb0978da72ee287b0143097db12f2e7e8d3ffc4647da07a383b0b7/charset_normalizer-3.4.6-cp314-cp314t-win32.whl", hash = "sha256:2a24157fa36980478dd1770b585c0f30d19e18f4fb0c47c13aa568f871718579", size = 155379, upload-time = "2026-03-15T18:52:27.05Z" }, - { url = "https://files.pythonhosted.org/packages/5d/7a/d269d834cb3a76291651256f3b9a5945e81d0a49ab9f4a498964e83c0416/charset_normalizer-3.4.6-cp314-cp314t-win_amd64.whl", hash = "sha256:cd5e2801c89992ed8c0a3f0293ae83c159a60d9a5d685005383ef4caca77f2c4", size = 169043, upload-time = "2026-03-15T18:52:28.502Z" }, - { url = "https://files.pythonhosted.org/packages/23/06/28b29fba521a37a8932c6a84192175c34d49f84a6d4773fa63d05f9aff22/charset_normalizer-3.4.6-cp314-cp314t-win_arm64.whl", hash = "sha256:47955475ac79cc504ef2704b192364e51d0d473ad452caedd0002605f780101c", size = 148523, upload-time = "2026-03-15T18:52:29.956Z" }, - { url = "https://files.pythonhosted.org/packages/2a/68/687187c7e26cb24ccbd88e5069f5ef00eba804d36dde11d99aad0838ab45/charset_normalizer-3.4.6-py3-none-any.whl", hash = "sha256:947cf925bc916d90adba35a64c82aace04fa39b46b52d4630ece166655905a69", size = 61455, upload-time = "2026-03-15T18:53:23.833Z" }, + { url = "https://files.pythonhosted.org/packages/ad/81/8e983840c6e5b93b33c2ba81aa3d52c2e42f0e9a690ce7607a2e61da4a5c/charset_normalizer-3.4.9-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cd6280cf040f233bd7d3407b743b4b4c74f70e8e1c4199cb112a62c941c0772a", size = 322240, upload-time = "2026-07-07T14:32:36.236Z" }, + { url = "https://files.pythonhosted.org/packages/de/d1/b4319dc3229d8272fba305e206fc0a148e2de8d4087917ce62ae6382f359/charset_normalizer-3.4.9-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa99adc8f081b475a12843953db36831eaf83ec33eb46a90629ca6a5de45a616", size = 216475, upload-time = "2026-07-07T14:32:38.142Z" }, + { url = "https://files.pythonhosted.org/packages/80/33/6c99c1b3e6b8bf730e1bc809b9a2608f224145069114c479a2e9e1494346/charset_normalizer-3.4.9-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c1225416b463483160e4af85d5fc3a9690ccb53fd4b1865a6437825f5ede3209", size = 238670, upload-time = "2026-07-07T14:32:39.658Z" }, + { url = "https://files.pythonhosted.org/packages/7f/f4/ffbb83546e1f198ecc70ecd372b65cf2b50f9068b380abd67640f17a8e18/charset_normalizer-3.4.9-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:16d10d789dd9bcca1173c95af82c58433122564b7bc39385124be735a35cbe99", size = 233476, upload-time = "2026-07-07T14:32:41.155Z" }, + { url = "https://files.pythonhosted.org/packages/e8/5f/b98b8da398637b551e427e7be922bdec19177dc54d6811dcdaa503f23aac/charset_normalizer-3.4.9-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9bb41182d93ea91f60b4bc8fbf4c820c69ef8a12ab2d917f3f1834f1acad07e8", size = 223817, upload-time = "2026-07-07T14:32:42.592Z" }, + { url = "https://files.pythonhosted.org/packages/36/31/a276bb2e66243072a3fd06fdcab9cbb61a305b02143d70d2bda21d888fa8/charset_normalizer-3.4.9-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:bcf74c1df76758a395bf0af608c04c82257523f55c9868b334f06270d0f2112b", size = 207974, upload-time = "2026-07-07T14:32:44.258Z" }, + { url = "https://files.pythonhosted.org/packages/5e/be/7ee4453d7e88dfbc4104ccd34900b9f2c7c17dac22881865fe0e82424a25/charset_normalizer-3.4.9-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b5314963fce9b0b12743891de876e724997864ee22aa496f903f426c7e2fa5b2", size = 221655, upload-time = "2026-07-07T14:32:45.64Z" }, + { url = "https://files.pythonhosted.org/packages/1d/85/181c652953eb5276d198f375b1dd641047392050098100a3a02d6534f657/charset_normalizer-3.4.9-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e9701d0049d92c16703a42771b98d560b95248949f23f8cf7b4eddd201814fb9", size = 219229, upload-time = "2026-07-07T14:32:47.376Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e7/aaf6da33fc9f4691cda8f7efbc9f69179d3d39ec8a4799baf273ee1d8db0/charset_normalizer-3.4.9-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:65a7ff3f705e57d392f7261b6d0550fe137c3019477431f1c355e0db0a7d3e15", size = 209704, upload-time = "2026-07-07T14:32:48.855Z" }, + { url = "https://files.pythonhosted.org/packages/63/01/f2fb3bd3a73be48b173ee0c6aa8d2497af97d5663a8c4c4b491de4c62f7a/charset_normalizer-3.4.9-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:79580094b00d1789d1f93ea55bc43cb2f611910c72235b7657f3482ddcc1b22d", size = 226243, upload-time = "2026-07-07T14:32:50.239Z" }, + { url = "https://files.pythonhosted.org/packages/c4/02/c57a22739fe05246b0b5783b3bfb6afaac4eebb46f3ececdfb2f048f780e/charset_normalizer-3.4.9-cp310-cp310-win32.whl", hash = "sha256:432786d3561e69aeeae6c7e8648964ce0ad05736120135601f87ac26b9c83381", size = 150935, upload-time = "2026-07-07T14:32:51.676Z" }, + { url = "https://files.pythonhosted.org/packages/37/8d/ca39a7559a4797505530d084fd3a49a2c959efbbbff146302fb7be4e3b35/charset_normalizer-3.4.9-cp310-cp310-win_amd64.whl", hash = "sha256:8c041122946b7ba21bb32c45b1aa57b1be35527690aeb3c5c234521085632eee", size = 162314, upload-time = "2026-07-07T14:32:53.193Z" }, + { url = "https://files.pythonhosted.org/packages/01/da/a44bd7a13d426e69e4894557106cd58669097bfad4a8681123b618fbfc5d/charset_normalizer-3.4.9-cp310-cp310-win_arm64.whl", hash = "sha256:375b83ed0aecfce76c16d198fbc21f3b11b337d68662bea0a995046682a11419", size = 153075, upload-time = "2026-07-07T14:32:54.554Z" }, + { url = "https://files.pythonhosted.org/packages/0b/e3/85ec501f206fb049259288c1f3506e53876937fb00edb47009348e66756b/charset_normalizer-3.4.9-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0e94703ec9684807f20cfb5eed95c70f67f2a8f21ad620146d7b5a13677b93e5", size = 317075, upload-time = "2026-07-07T14:32:56.021Z" }, + { url = "https://files.pythonhosted.org/packages/c3/69/2a5385192e67175f7d8bd5ce4f57c24bc956439adeae5c13a99aa28a53d1/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a441ea71902098ffe78c5abe6c494f44160b4af614ed16c3d9a3b1d17fd8ee2", size = 213837, upload-time = "2026-07-07T14:32:57.78Z" }, + { url = "https://files.pythonhosted.org/packages/b3/46/03ddc7da576d814fe0a36dd1f0fd3258e95404b4b2e3c026b7923d7e133f/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:304b13570067b2547562e308af560b3963857b1fa90bd6afd978130130fe2d6a", size = 235503, upload-time = "2026-07-07T14:32:59.205Z" }, + { url = "https://files.pythonhosted.org/packages/4e/6e/de0229a7ef40f6f9d28a837eebf4ec47bdca5dab4e900c84f22919af636a/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4773092f8019072343a7447203308b176e10199920eb02d6195e81bbb3274c29", size = 229944, upload-time = "2026-07-07T14:33:00.803Z" }, + { url = "https://files.pythonhosted.org/packages/a5/34/49b9060e8418b14fb5cba9cf6bfb383111e2538a03a1fb18e66a95aeb3d5/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:04ce310cb89c15df659582aee80a0603788732a5e017d5bd5c81158106ce249c", size = 221276, upload-time = "2026-07-07T14:33:02.199Z" }, + { url = "https://files.pythonhosted.org/packages/44/95/80282cce0fae9c3061203d723ee87da996aed79679e65d8935050ee7ca1f/charset_normalizer-3.4.9-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:c0323c9daef75ef2e5083624b4585018a0c9d5e3b40f607eed81a311270b934b", size = 205260, upload-time = "2026-07-07T14:33:03.698Z" }, + { url = "https://files.pythonhosted.org/packages/0c/74/2f62c8821b969ea3bd67cc2e6976834f48ca5d12664d2559ebcd9bcfbed7/charset_normalizer-3.4.9-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:871ff67ea1aad4dfd91736464934d56b32dac49f9fbe16cddba36198a7b3a0db", size = 217786, upload-time = "2026-07-07T14:33:05.12Z" }, + { url = "https://files.pythonhosted.org/packages/d9/8d/feabb82cb49fcad14515b1d7d1ca4787b0da7fc723a212bf89bc9e0fac52/charset_normalizer-3.4.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:67830fc78e67501f47bb950471b2dcb9b35b140084429318e862895a8e89c993", size = 216798, upload-time = "2026-07-07T14:33:06.629Z" }, + { url = "https://files.pythonhosted.org/packages/a5/ff/c946d63bc3786d5b84d960b0f7ab7e25b828486a946b5aa997625bcaf6a6/charset_normalizer-3.4.9-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:3d92613ec25e43b05f042302531ec0f00b8445190e43325880cbd6ab7c2581da", size = 206429, upload-time = "2026-07-07T14:33:08.006Z" }, + { url = "https://files.pythonhosted.org/packages/af/ba/5e5007c370702f85d2ef75791fac7943ed41e080364a673b20142e430e3e/charset_normalizer-3.4.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:280081916dc341820640489a66e4696049401ef1cf6dd672f672e70ad915aca3", size = 223066, upload-time = "2026-07-07T14:33:09.783Z" }, + { url = "https://files.pythonhosted.org/packages/83/d5/9096aa3cf532dfad237861544eb47a0f20d5adbf1039760fed8eaae935d9/charset_normalizer-3.4.9-cp311-cp311-win32.whl", hash = "sha256:ac351b3b8014eead140e77e9717e2992c6bbe30b63bc3422422eb84865412e3d", size = 150456, upload-time = "2026-07-07T14:33:11.217Z" }, + { url = "https://files.pythonhosted.org/packages/ed/a1/e29995109e455dc8eff8d0fac6ae509be39561318a7cfeac5d33ad029213/charset_normalizer-3.4.9-cp311-cp311-win_amd64.whl", hash = "sha256:6366a16e1a25018694d6a5d784d09b046edc9eac40ea2b54065c3052672516a1", size = 161410, upload-time = "2026-07-07T14:33:12.743Z" }, + { url = "https://files.pythonhosted.org/packages/4f/8d/1569f4d0032d6ba2a4fe4591c35bf87868c600c41a71eb5c2e1ffa8464c2/charset_normalizer-3.4.9-cp311-cp311-win_arm64.whl", hash = "sha256:1d22856ffbe153a602df38e4a5464f0b748a54002e0d69ac6d2ad0a197cc99ec", size = 152649, upload-time = "2026-07-07T14:33:14.173Z" }, + { url = "https://files.pythonhosted.org/packages/70/4a/ecbd131485c07fcdfad54e28946d513e3da22ef3b4bd854dcafae54ec739/charset_normalizer-3.4.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:45b0cc4e3556cd875e09102988d1ab8356c998b596c9fced84547c8138b487a0", size = 319300, upload-time = "2026-07-07T14:33:15.666Z" }, + { url = "https://files.pythonhosted.org/packages/ec/96/5d9364e3342d69f3a045e1777bc47c85c383e6e9466d561b33fdb419d1f9/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9b2aff1c7b3884512b9512c3eaadd9bab39fb45042ffaaa1dd08ff2b9f8109d9", size = 215802, upload-time = "2026-07-07T14:33:17.031Z" }, + { url = "https://files.pythonhosted.org/packages/4b/4c/5361f9aa7f2cb58d94f2ab831b3d493f69efb1d239654b4744e3c09527cb/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9104ed0bd76a429d46f9ec0dbc9b08ad1d2dcdf2b00a5a0daa1c145329b35b44", size = 237171, upload-time = "2026-07-07T14:33:18.576Z" }, + { url = "https://files.pythonhosted.org/packages/50/78/ce342ca4ff30b2eb49fe6d9578df85974f90c67d294113e94efdd9664cbd/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7b86a2b16095d250c6f58b3d9b2eee6f4147754344f3dab0922f7c9bf7d226c9", size = 233075, upload-time = "2026-07-07T14:33:20.084Z" }, + { url = "https://files.pythonhosted.org/packages/01/c4/4fa4c8b3097a11f3c5f09a35b72ed6855fb1d332469504962ab7bafcc702/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e226f6218febc71f6c1fc2fafb91c226f75bdc1d8fb12d66823716e891608fd", size = 224256, upload-time = "2026-07-07T14:33:21.747Z" }, + { url = "https://files.pythonhosted.org/packages/87/3a/ad914516df7e358a81aae018caa5e0470ba827fa6d763b1d2e87d920a5f6/charset_normalizer-3.4.9-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:90c44bc373b7687f6948b693cceaea1348ae0975d7474746559494468e3c1d84", size = 208784, upload-time = "2026-07-07T14:33:23.313Z" }, + { url = "https://files.pythonhosted.org/packages/d7/74/3c12f9755717dfe5c5c87da63f35d765fa0c00382ec26bf23f7fae34f2ba/charset_normalizer-3.4.9-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cdef90ae47919cae358d8ab15797a800ed41da7aba5d72419fb510729e2ed4b", size = 219928, upload-time = "2026-07-07T14:33:24.814Z" }, + { url = "https://files.pythonhosted.org/packages/33/9a/895095b83e7907abd6d3d99aad3a38ad0d9686cc186cb0c94c24320fe63e/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:60f44ade2cf573dad7a277e6f8ca9a51a21dda572b13bd7d8539bb3cd5dbedde", size = 218489, upload-time = "2026-07-07T14:33:26.42Z" }, + { url = "https://files.pythonhosted.org/packages/a1/34/ef5c05f412f42520d7709b7d3784d19640839eb7366ded1755511585429f/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:a1786910334ed46ab1dd73222f2cd1e05c2c3bb39f6dddb4f8b36fc382058a39", size = 210267, upload-time = "2026-07-07T14:33:27.952Z" }, + { url = "https://files.pythonhosted.org/packages/83/dc/9b29fa4412b318bf3bfea985c35d67eb55e04b59a7c3f2237168b0e0be6f/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:03d07803992c6c7bbc976327f34b18b6160327fc81cb82c9d504720ac0be3b62", size = 226030, upload-time = "2026-07-07T14:33:29.397Z" }, + { url = "https://files.pythonhosted.org/packages/0e/42/6dbc00b8cd16011691203e33570fa42ed5746599a2e878112d16eab403a3/charset_normalizer-3.4.9-cp312-cp312-win32.whl", hash = "sha256:78841cccf1af7b40f6f716338d50c0902dbe88d9f800b3c973b7a9a0a693a642", size = 151185, upload-time = "2026-07-07T14:33:30.781Z" }, + { url = "https://files.pythonhosted.org/packages/80/cc/f920afd1a23c58ccd53c1d36085a71893a4737ff5e66e0371efab6809850/charset_normalizer-3.4.9-cp312-cp312-win_amd64.whl", hash = "sha256:4b3dac63058cc36820b0dd072f89898604e2d39686fe05321729d00d8ac185a0", size = 162557, upload-time = "2026-07-07T14:33:32.176Z" }, + { url = "https://files.pythonhosted.org/packages/f0/e6/0386d43a261ff4e4b30c5857af7df877254b46bec7b9d1b74b6bf969a90b/charset_normalizer-3.4.9-cp312-cp312-win_arm64.whl", hash = "sha256:78fa18e436a1a0e58dbd7e02fc4473f3f32cceb12df9dfca542d075961c307d2", size = 152665, upload-time = "2026-07-07T14:33:33.711Z" }, + { url = "https://files.pythonhosted.org/packages/b2/06/97ec2aeae780b31d742b6352218b43841a6871e2564578ca522dce4a45c3/charset_normalizer-3.4.9-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:440eede837960000d74978f0eba527be106b5b9aee0daf779d395276ed0b0614", size = 317688, upload-time = "2026-07-07T14:33:35.408Z" }, + { url = "https://files.pythonhosted.org/packages/d0/39/8ff066c672434225f8d25f8b739f992af250944392173dcc88362681c9bf/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21e764fd1e70b6a3e205a0e46f3051701f98a8cb3fad66eeb80e48bb502f8698", size = 214982, upload-time = "2026-07-07T14:33:36.996Z" }, + { url = "https://files.pythonhosted.org/packages/92/8f/3a47a3667c83c2df9483d91644c6c107de3bf8874aa1793da9d3012eb986/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e4fd89cc178bced6ad29cb3e6dd4aa63fa5017c3524dbd0b25998fb64a87cc8b", size = 236460, upload-time = "2026-07-07T14:33:38.536Z" }, + { url = "https://files.pythonhosted.org/packages/f1/60/b22cdbee7e4013dab8b0d7647fc6181120fbbbc8f7025c226d15bd5a47fc/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bd47ba7fc3ca94896759ea0109775132d3e7ab921fbf54038e1bab2e46c313c9", size = 232003, upload-time = "2026-07-07T14:33:40.059Z" }, + { url = "https://files.pythonhosted.org/packages/ea/f8/72eb13dcabe7257035cea8aefd922caad2f110d252bf9f67c4c2ca763aee/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:84fd18bcc17526fc2b3c1af7d2b9217d32c9c04448c16ec693b9b4f1985c3d33", size = 223149, upload-time = "2026-07-07T14:33:41.631Z" }, + { url = "https://files.pythonhosted.org/packages/b0/3e/faee8f9de92b14ee1198e9163252bb15efee7301b31256a3b6d9ebfdd0dd/charset_normalizer-3.4.9-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:5b10cd92fc5c498b35a8635df6d5a100207f88b63a4dc1de7ef9a548e1e2cd63", size = 207901, upload-time = "2026-07-07T14:33:43.209Z" }, + { url = "https://files.pythonhosted.org/packages/3a/25/45f30093ae27dd7b92a793b61882a38685f993700113ca36e0c9c14965e1/charset_normalizer-3.4.9-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a4fbdde9dd4a9ce5fd52c2b3a347bb50cc89483ef783f1cb00d408c13f7a96c0", size = 219176, upload-time = "2026-07-07T14:33:44.725Z" }, + { url = "https://files.pythonhosted.org/packages/48/18/c8f397329c35e32f6a837e488986f4ae03bd2abebc453b48714991630c2f/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:416c229f77e5ea25b3dfd4b582f8d73d7e43c22320302b9ab128a2d3a0b38efe", size = 217356, upload-time = "2026-07-07T14:33:46.192Z" }, + { url = "https://files.pythonhosted.org/packages/86/7e/5ce0bba863470fd1902d5e5843968951bddf38abe4742fc97116ef4598b3/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:75286256590a6320cf106a0d28970d3560aad9ee09aa7b34fb40524792436d35", size = 209614, upload-time = "2026-07-07T14:33:47.705Z" }, + { url = "https://files.pythonhosted.org/packages/6c/ef/2473d3c4d869155be4af1191111d59c4d5c4e0173026f7e85b176e23bf65/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:69b157c5d3292bcd443faca052f3096f637f1e074b98212a933c074ae23dc3b8", size = 224991, upload-time = "2026-07-07T14:33:49.238Z" }, + { url = "https://files.pythonhosted.org/packages/d0/a3/53ddae3db108a088156aa8ddfafd411ebbc1340f48c5573f697b27f69a39/charset_normalizer-3.4.9-cp313-cp313-win32.whl", hash = "sha256:51307f5c71007673a2bf8232ad973483d281e74cb99c8c5a990af1eefa6277d9", size = 150622, upload-time = "2026-07-07T14:33:50.711Z" }, + { url = "https://files.pythonhosted.org/packages/e8/ef/6953a77c7cf2c2ff9998e6f575ab3e380119f100223381565a4f94c1f836/charset_normalizer-3.4.9-cp313-cp313-win_amd64.whl", hash = "sha256:fe2c7201c642b7c308f1675355ad7ff7b66acfe3541625efe5a3ad38f29d6115", size = 161947, upload-time = "2026-07-07T14:33:52.197Z" }, + { url = "https://files.pythonhosted.org/packages/6e/fb/d560d1d1555debbfe7849d9cac6145c1b537709d79576bf22557ed803b82/charset_normalizer-3.4.9-cp313-cp313-win_arm64.whl", hash = "sha256:611057cc5d5c0afc743ba8be6bd828c17e0aaa8643f9d0a9b9bb7dea80eb8012", size = 152594, upload-time = "2026-07-07T14:33:53.486Z" }, + { url = "https://files.pythonhosted.org/packages/7e/8d/496817fa0944239ecae662dd57ea765cfeaec6a735f9f025d4b7b72e7143/charset_normalizer-3.4.9-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:0327fcd59a935777d83410750c50600ee9571af2846f71ce40f25b13da1ef380", size = 317253, upload-time = "2026-07-07T14:33:54.994Z" }, + { url = "https://files.pythonhosted.org/packages/2b/f9/ef4a69ea338ad3c0deceea0f5f7d2380ae8b52132b06d652cb0d2cd86706/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a79d9f4d8001473a30c163556b3c3bfebec837495a412dde78b51672f6134f9", size = 215898, upload-time = "2026-07-07T14:33:56.334Z" }, + { url = "https://files.pythonhosted.org/packages/8c/e7/5ddfd76fc061eb52de219658a4aa431cbacadf0a0219c8854f00da50d289/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:33bdcc2a32c0a0e861f60841a512c8acc658c87c2ac59d89e3a46dacf7d866e4", size = 236718, upload-time = "2026-07-07T14:33:57.9Z" }, + { url = "https://files.pythonhosted.org/packages/49/ba/768fa3f36048d81c477a0ce61f813bc1454d80917ccfe550abd9f44f5e24/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f840ed6d8ecba8255df8c42b87fadeda98ddfc6eeec05e2dc66e26d46dd6f58a", size = 232519, upload-time = "2026-07-07T14:33:59.811Z" }, + { url = "https://files.pythonhosted.org/packages/f4/c4/b3e049d2aa3766180c78507110543d9d50894cc97f57de543f1be521dcdc/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c25fe15c70c59eb7c5ce8c06a1f3fa1da0ecc5ea1e7a5922c40fd2fa9b0d5046", size = 223143, upload-time = "2026-07-07T14:34:01.517Z" }, + { url = "https://files.pythonhosted.org/packages/19/79/55c32d06d76ae4feafe053f061f3e3ab70bcf19f4007797ce8c3efda7830/charset_normalizer-3.4.9-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:f7fb7d750cfa0a070d2c24e831fd3481019a60dd317ea2b39acbcebc08b6ed81", size = 206742, upload-time = "2026-07-07T14:34:03.04Z" }, + { url = "https://files.pythonhosted.org/packages/10/e0/47c079dd82d217c807479cd59ffd30af56307ea31c108b75758970459ad3/charset_normalizer-3.4.9-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4d1c96a7a18b9690a4d46df09e3e3382406ae3213727cd1019ebade1c4a81917", size = 219191, upload-time = "2026-07-07T14:34:04.657Z" }, + { url = "https://files.pythonhosted.org/packages/42/ab/b9bc2e77d6b44a7e46ef62ec5cac1c9a6ba7b9135a5d560f002696ec9995/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a4cfde78a9f2880208d16a93b795726a3017d5977e08d1e162a7a31322479c41", size = 218328, upload-time = "2026-07-07T14:34:06.115Z" }, + { url = "https://files.pythonhosted.org/packages/f1/78/c9c71d599f5aa2d42bcdd35cbbd46d7f535351a57e40ff7d8e5a7e219401/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:d4d6fcde76f94f5cb9e43e9e9a61f16dacefd228cbbf6f1a09bd9b219a92f1a1", size = 207406, upload-time = "2026-07-07T14:34:07.554Z" }, + { url = "https://files.pythonhosted.org/packages/f6/39/c914445c321a845097ce4f6ac7de9a18228a77b766272125a1ce00d851eb/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:898f0e9068ca27d37f8e83a5b962821df851532e6c4a7d615c1c033f9da6eedf", size = 225157, upload-time = "2026-07-07T14:34:09.061Z" }, + { url = "https://files.pythonhosted.org/packages/9b/f2/c0d4b8508565a36bc5c624e88ed297f5b0b1095011034d7f5b83a69908b5/charset_normalizer-3.4.9-cp314-cp314-win32.whl", hash = "sha256:c1c948747b03be832dceed96ca815cef7360de9aa19d37c730f8e3f6101aca48", size = 151095, upload-time = "2026-07-07T14:34:10.901Z" }, + { url = "https://files.pythonhosted.org/packages/49/fd/a1d26144398c67486422a72bf5812cda22cb4ccfcd95a290fb41ceb4b8e2/charset_normalizer-3.4.9-cp314-cp314-win_amd64.whl", hash = "sha256:16b65ea0f2465b6fb52aa22de5eca612aa964ddfec00a912e26f4656cbef890b", size = 162796, upload-time = "2026-07-07T14:34:12.47Z" }, + { url = "https://files.pythonhosted.org/packages/20/95/d75e82f8ce9fd323ebf059c16c9aadefb22a1ecde13b7840b35835e4886c/charset_normalizer-3.4.9-cp314-cp314-win_arm64.whl", hash = "sha256:40a126142a56b2dfc0aacbad1de8310cbf60da7656db0e6b16eebd48e3e93519", size = 153334, upload-time = "2026-07-07T14:34:14.044Z" }, + { url = "https://files.pythonhosted.org/packages/00/5e/17398df3a139985ba9d11ed072531986f408c8fca952835ef1ab1820c02b/charset_normalizer-3.4.9-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:609b3ba8fcc0fb5ab7af00719d0fb6ad0cb518e48e7712d12fd68f1327951198", size = 338848, upload-time = "2026-07-07T14:34:15.688Z" }, + { url = "https://files.pythonhosted.org/packages/cd/91/7253a32e86b7e1d1239b1b36ba6dd0f021a21107ab33054b53119cc083b9/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51447e9aa2684679af07ca5021c3db526e0284347ebf4ffcec1154c3350cfe32", size = 223022, upload-time = "2026-07-07T14:34:17.248Z" }, + { url = "https://files.pythonhosted.org/packages/cb/32/2e64bd2be10e89c61e57ebe6a93fd98ae88eb7ebe414b5121f22c96c69eb/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cc1b0fff8ead343dae06305f954eb8468ba0ec1a97881f42489d198e4ce3c632", size = 241590, upload-time = "2026-07-07T14:34:18.813Z" }, + { url = "https://files.pythonhosted.org/packages/3d/ef/d96ec496cfea0c21db43b0ad03891308b02388d054cc902cf0e5a1ad6a88/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fa36ec09ef71d158186bc79e359ff5fdd6e7996fe8ab638f00d6b93139ba4fcf", size = 239584, upload-time = "2026-07-07T14:34:20.52Z" }, + { url = "https://files.pythonhosted.org/packages/d4/ce/9af95f7876194bd7a14e3dfe4a4de2e0bff02666a3910d72beafd06cc297/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:df115d4d83168fdf2cae48ef1ff6d1cb4c466364e30861b37121de0f3bf1b990", size = 230224, upload-time = "2026-07-07T14:34:22.189Z" }, + { url = "https://files.pythonhosted.org/packages/52/94/af74dde74a3996bd959c350709bfe50e297823d70a8c1cbd54b838880863/charset_normalizer-3.4.9-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:f86c6358749bd4fda175388691e3ba8c46e24c5347d0afd20f9b7edfc9faf07d", size = 212667, upload-time = "2026-07-07T14:34:23.857Z" }, + { url = "https://files.pythonhosted.org/packages/ee/f0/f1c4fe746c395922961b5916ed1d7d6e7d4c84851d19ed43cc89980ec953/charset_normalizer-3.4.9-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:32286a2c8d167e897177b673176c1e3e00d4057caf5d2b64eef9a3666b03018e", size = 227179, upload-time = "2026-07-07T14:34:25.586Z" }, + { url = "https://files.pythonhosted.org/packages/e4/56/6c745619ac397e8871e2bcd3cea1eec86b877488f33888b3aef5c3ed506e/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:83aed2c10721ddd90f68140685391b50811a880af20654c59af6b6c66c40513c", size = 225372, upload-time = "2026-07-07T14:34:27.212Z" }, + { url = "https://files.pythonhosted.org/packages/78/ad/98aae8630ac71f16711968e38a5acfecce41b778bf2f0312851020f565a8/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cd6c3d4b783c556fa00bf540854e42f135e2f256abd29669fcd0da0f2dec79c2", size = 215222, upload-time = "2026-07-07T14:34:28.774Z" }, + { url = "https://files.pythonhosted.org/packages/f7/40/9593d54209765207a7f11073c06494c1721e4ca4a0a426c597679bf7f91e/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ee2f2a527e3c1a6e6411eb4209642e138b544a2d72fe5d0d76daf77b24063534", size = 231958, upload-time = "2026-07-07T14:34:30.345Z" }, + { url = "https://files.pythonhosted.org/packages/b1/27/693ee5e8a18191eb38647360c51cd505013e2bd3b366aa43fd5344c21e3c/charset_normalizer-3.4.9-cp314-cp314t-win32.whl", hash = "sha256:0d861473f743244d349b50f850d10eb87aeb22bbdcc8e64f79273c94af5a8226", size = 155580, upload-time = "2026-07-07T14:34:31.884Z" }, + { url = "https://files.pythonhosted.org/packages/80/3f/bd97d3d9c613013d07cb7733d299385b41df37f0471310f5a73dc359f0b8/charset_normalizer-3.4.9-cp314-cp314t-win_amd64.whl", hash = "sha256:9b8e0f3107e2200b76f6054de99016eac3ee6762713587b36baaa7e4bd2ae177", size = 167620, upload-time = "2026-07-07T14:34:33.438Z" }, + { url = "https://files.pythonhosted.org/packages/3d/c6/eee9dca4439b1061f76373f06ea855678cc4a64c1c3c90b50e479edbb8eb/charset_normalizer-3.4.9-cp314-cp314t-win_arm64.whl", hash = "sha256:19ac87f93086ce37b86e098888555c4b4bc48102279bae3350098c0ed664b501", size = 158037, upload-time = "2026-07-07T14:34:35.018Z" }, + { url = "https://files.pythonhosted.org/packages/98/2b/f97f1c193fb855c345d678f5077d6926034db0722df74c8f057020e05a25/charset_normalizer-3.4.9-py3-none-any.whl", hash = "sha256:68e5f26a1ad57ded6d1cfb85331d1c1a195314756471d97758c48498bb4dcdf5", size = 64538, upload-time = "2026-07-07T14:34:56.993Z" }, ] [[package]] name = "click" -version = "8.3.1" +version = "8.4.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } +sdist = { url = "https://files.pythonhosted.org/packages/76/d4/81420972a676e8ffea40450d8c8c92943e7218a78fe9b64359836cc9876b/click-8.4.2.tar.gz", hash = "sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6", size = 338000, upload-time = "2026-06-24T17:45:15.148Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, + { url = "https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl", hash = "sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76", size = 119243, upload-time = "2026-06-24T17:45:13.73Z" }, ] [[package]] @@ -517,115 +543,100 @@ wheels = [ [[package]] name = "coverage" -version = "7.13.5" +version = "7.15.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9d/e0/70553e3000e345daff267cec284ce4cbf3fc141b6da229ac52775b5428f1/coverage-7.13.5.tar.gz", hash = "sha256:c81f6515c4c40141f83f502b07bbfa5c240ba25bbe73da7b33f1e5b6120ff179", size = 915967, upload-time = "2026-03-17T10:33:18.341Z" } +sdist = { url = "https://files.pythonhosted.org/packages/76/d0/55fe630f4cf94e3fcba868240fad8c8cdd1f764e2a932f8926347e6ec4cd/coverage-7.15.2.tar.gz", hash = "sha256:3df60dc267f0a2ca23cb7a9ab1109c62b9335ffbf519fcfe167157c28c09b81d", size = 927741, upload-time = "2026-07-15T18:56:19.558Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/69/33/e8c48488c29a73fd089f9d71f9653c1be7478f2ad6b5bc870db11a55d23d/coverage-7.13.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e0723d2c96324561b9aa76fb982406e11d93cdb388a7a7da2b16e04719cf7ca5", size = 219255, upload-time = "2026-03-17T10:29:51.081Z" }, - { url = "https://files.pythonhosted.org/packages/da/bd/b0ebe9f677d7f4b74a3e115eec7ddd4bcf892074963a00d91e8b164a6386/coverage-7.13.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:52f444e86475992506b32d4e5ca55c24fc88d73bcbda0e9745095b28ef4dc0cf", size = 219772, upload-time = "2026-03-17T10:29:52.867Z" }, - { url = "https://files.pythonhosted.org/packages/48/cc/5cb9502f4e01972f54eedd48218bb203fe81e294be606a2bc93970208013/coverage-7.13.5-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:704de6328e3d612a8f6c07000a878ff38181ec3263d5a11da1db294fa6a9bdf8", size = 246532, upload-time = "2026-03-17T10:29:54.688Z" }, - { url = "https://files.pythonhosted.org/packages/7d/d8/3217636d86c7e7b12e126e4f30ef1581047da73140614523af7495ed5f2d/coverage-7.13.5-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a1a6d79a14e1ec1832cabc833898636ad5f3754a678ef8bb4908515208bf84f4", size = 248333, upload-time = "2026-03-17T10:29:56.221Z" }, - { url = "https://files.pythonhosted.org/packages/2b/30/2002ac6729ba2d4357438e2ed3c447ad8562866c8c63fc16f6dfc33afe56/coverage-7.13.5-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:79060214983769c7ba3f0cee10b54c97609dca4d478fa1aa32b914480fd5738d", size = 250211, upload-time = "2026-03-17T10:29:57.938Z" }, - { url = "https://files.pythonhosted.org/packages/6c/85/552496626d6b9359eb0e2f86f920037c9cbfba09b24d914c6e1528155f7d/coverage-7.13.5-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:356e76b46783a98c2a2fe81ec79df4883a1e62895ea952968fb253c114e7f930", size = 252125, upload-time = "2026-03-17T10:29:59.388Z" }, - { url = "https://files.pythonhosted.org/packages/44/21/40256eabdcbccdb6acf6b381b3016a154399a75fe39d406f790ae84d1f3c/coverage-7.13.5-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0cef0cdec915d11254a7f549c1170afecce708d30610c6abdded1f74e581666d", size = 247219, upload-time = "2026-03-17T10:30:01.199Z" }, - { url = "https://files.pythonhosted.org/packages/b1/e8/96e2a6c3f21a0ea77d7830b254a1542d0328acc8d7bdf6a284ba7e529f77/coverage-7.13.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:dc022073d063b25a402454e5712ef9e007113e3a676b96c5f29b2bda29352f40", size = 248248, upload-time = "2026-03-17T10:30:03.317Z" }, - { url = "https://files.pythonhosted.org/packages/da/ba/8477f549e554827da390ec659f3c38e4b6d95470f4daafc2d8ff94eaa9c2/coverage-7.13.5-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:9b74db26dfea4f4e50d48a4602207cd1e78be33182bc9cbf22da94f332f99878", size = 246254, upload-time = "2026-03-17T10:30:04.832Z" }, - { url = "https://files.pythonhosted.org/packages/55/59/bc22aef0e6aa179d5b1b001e8b3654785e9adf27ef24c93dc4228ebd5d68/coverage-7.13.5-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:ad146744ca4fd09b50c482650e3c1b1f4dfa1d4792e0a04a369c7f23336f0400", size = 250067, upload-time = "2026-03-17T10:30:06.535Z" }, - { url = "https://files.pythonhosted.org/packages/de/1b/c6a023a160806a5137dca53468fd97530d6acad24a22003b1578a9c2e429/coverage-7.13.5-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:c555b48be1853fe3997c11c4bd521cdd9a9612352de01fa4508f16ec341e6fe0", size = 246521, upload-time = "2026-03-17T10:30:08.486Z" }, - { url = "https://files.pythonhosted.org/packages/2d/3f/3532c85a55aa2f899fa17c186f831cfa1aa434d88ff792a709636f64130e/coverage-7.13.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7034b5c56a58ae5e85f23949d52c14aca2cfc6848a31764995b7de88f13a1ea0", size = 247126, upload-time = "2026-03-17T10:30:09.966Z" }, - { url = "https://files.pythonhosted.org/packages/aa/2e/b9d56af4a24ef45dfbcda88e06870cb7d57b2b0bfa3a888d79b4c8debd76/coverage-7.13.5-cp310-cp310-win32.whl", hash = "sha256:eb7fdf1ef130660e7415e0253a01a7d5a88c9c4d158bcf75cbbd922fd65a5b58", size = 221860, upload-time = "2026-03-17T10:30:11.393Z" }, - { url = "https://files.pythonhosted.org/packages/9f/cc/d938417e7a4d7f0433ad4edee8bb2acdc60dc7ac5af19e2a07a048ecbee3/coverage-7.13.5-cp310-cp310-win_amd64.whl", hash = "sha256:3e1bb5f6c78feeb1be3475789b14a0f0a5b47d505bfc7267126ccbd50289999e", size = 222788, upload-time = "2026-03-17T10:30:12.886Z" }, - { url = "https://files.pythonhosted.org/packages/4b/37/d24c8f8220ff07b839b2c043ea4903a33b0f455abe673ae3c03bbdb7f212/coverage-7.13.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:66a80c616f80181f4d643b0f9e709d97bcea413ecd9631e1dedc7401c8e6695d", size = 219381, upload-time = "2026-03-17T10:30:14.68Z" }, - { url = "https://files.pythonhosted.org/packages/35/8b/cd129b0ca4afe886a6ce9d183c44d8301acbd4ef248622e7c49a23145605/coverage-7.13.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:145ede53ccbafb297c1c9287f788d1bc3efd6c900da23bf6931b09eafc931587", size = 219880, upload-time = "2026-03-17T10:30:16.231Z" }, - { url = "https://files.pythonhosted.org/packages/55/2f/e0e5b237bffdb5d6c530ce87cc1d413a5b7d7dfd60fb067ad6d254c35c76/coverage-7.13.5-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0672854dc733c342fa3e957e0605256d2bf5934feeac328da9e0b5449634a642", size = 250303, upload-time = "2026-03-17T10:30:17.748Z" }, - { url = "https://files.pythonhosted.org/packages/92/be/b1afb692be85b947f3401375851484496134c5554e67e822c35f28bf2fbc/coverage-7.13.5-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ec10e2a42b41c923c2209b846126c6582db5e43a33157e9870ba9fb70dc7854b", size = 252218, upload-time = "2026-03-17T10:30:19.804Z" }, - { url = "https://files.pythonhosted.org/packages/da/69/2f47bb6fa1b8d1e3e5d0c4be8ccb4313c63d742476a619418f85740d597b/coverage-7.13.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be3d4bbad9d4b037791794ddeedd7d64a56f5933a2c1373e18e9e568b9141686", size = 254326, upload-time = "2026-03-17T10:30:21.321Z" }, - { url = "https://files.pythonhosted.org/packages/d5/d0/79db81da58965bd29dabc8f4ad2a2af70611a57cba9d1ec006f072f30a54/coverage-7.13.5-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4d2afbc5cc54d286bfb54541aa50b64cdb07a718227168c87b9e2fb8f25e1743", size = 256267, upload-time = "2026-03-17T10:30:23.094Z" }, - { url = "https://files.pythonhosted.org/packages/e5/32/d0d7cc8168f91ddab44c0ce4806b969df5f5fdfdbb568eaca2dbc2a04936/coverage-7.13.5-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3ad050321264c49c2fa67bb599100456fc51d004b82534f379d16445da40fb75", size = 250430, upload-time = "2026-03-17T10:30:25.311Z" }, - { url = "https://files.pythonhosted.org/packages/4d/06/a055311d891ddbe231cd69fdd20ea4be6e3603ffebddf8704b8ca8e10a3c/coverage-7.13.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7300c8a6d13335b29bb76d7651c66af6bd8658517c43499f110ddc6717bfc209", size = 252017, upload-time = "2026-03-17T10:30:27.284Z" }, - { url = "https://files.pythonhosted.org/packages/d6/f6/d0fd2d21e29a657b5f77a2fe7082e1568158340dceb941954f776dce1b7b/coverage-7.13.5-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:eb07647a5738b89baab047f14edd18ded523de60f3b30e75c2acc826f79c839a", size = 250080, upload-time = "2026-03-17T10:30:29.481Z" }, - { url = "https://files.pythonhosted.org/packages/4e/ab/0d7fb2efc2e9a5eb7ddcc6e722f834a69b454b7e6e5888c3a8567ecffb31/coverage-7.13.5-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:9adb6688e3b53adffefd4a52d72cbd8b02602bfb8f74dcd862337182fd4d1a4e", size = 253843, upload-time = "2026-03-17T10:30:31.301Z" }, - { url = "https://files.pythonhosted.org/packages/ba/6f/7467b917bbf5408610178f62a49c0ed4377bb16c1657f689cc61470da8ce/coverage-7.13.5-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7c8d4bc913dd70b93488d6c496c77f3aff5ea99a07e36a18f865bca55adef8bd", size = 249802, upload-time = "2026-03-17T10:30:33.358Z" }, - { url = "https://files.pythonhosted.org/packages/75/2c/1172fb689df92135f5bfbbd69fc83017a76d24ea2e2f3a1154007e2fb9f8/coverage-7.13.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0e3c426ffc4cd952f54ee9ffbdd10345709ecc78a3ecfd796a57236bfad0b9b8", size = 250707, upload-time = "2026-03-17T10:30:35.2Z" }, - { url = "https://files.pythonhosted.org/packages/67/21/9ac389377380a07884e3b48ba7a620fcd9dbfaf1d40565facdc6b36ec9ef/coverage-7.13.5-cp311-cp311-win32.whl", hash = "sha256:259b69bb83ad9894c4b25be2528139eecba9a82646ebdda2d9db1ba28424a6bf", size = 221880, upload-time = "2026-03-17T10:30:36.775Z" }, - { url = "https://files.pythonhosted.org/packages/af/7f/4cd8a92531253f9d7c1bbecd9fa1b472907fb54446ca768c59b531248dc5/coverage-7.13.5-cp311-cp311-win_amd64.whl", hash = "sha256:258354455f4e86e3e9d0d17571d522e13b4e1e19bf0f8596bcf9476d61e7d8a9", size = 222816, upload-time = "2026-03-17T10:30:38.891Z" }, - { url = "https://files.pythonhosted.org/packages/12/a6/1d3f6155fb0010ca68eba7fe48ca6c9da7385058b77a95848710ecf189b1/coverage-7.13.5-cp311-cp311-win_arm64.whl", hash = "sha256:bff95879c33ec8da99fc9b6fe345ddb5be6414b41d6d1ad1c8f188d26f36e028", size = 221483, upload-time = "2026-03-17T10:30:40.463Z" }, - { url = "https://files.pythonhosted.org/packages/a0/c3/a396306ba7db865bf96fc1fb3b7fd29bcbf3d829df642e77b13555163cd6/coverage-7.13.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:460cf0114c5016fa841214ff5564aa4864f11948da9440bc97e21ad1f4ba1e01", size = 219554, upload-time = "2026-03-17T10:30:42.208Z" }, - { url = "https://files.pythonhosted.org/packages/a6/16/a68a19e5384e93f811dccc51034b1fd0b865841c390e3c931dcc4699e035/coverage-7.13.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0e223ce4b4ed47f065bfb123687686512e37629be25cc63728557ae7db261422", size = 219908, upload-time = "2026-03-17T10:30:43.906Z" }, - { url = "https://files.pythonhosted.org/packages/29/72/20b917c6793af3a5ceb7fb9c50033f3ec7865f2911a1416b34a7cfa0813b/coverage-7.13.5-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6e3370441f4513c6252bf042b9c36d22491142385049243253c7e48398a15a9f", size = 251419, upload-time = "2026-03-17T10:30:45.545Z" }, - { url = "https://files.pythonhosted.org/packages/8c/49/cd14b789536ac6a4778c453c6a2338bc0a2fb60c5a5a41b4008328b9acc1/coverage-7.13.5-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:03ccc709a17a1de074fb1d11f217342fb0d2b1582ed544f554fc9fc3f07e95f5", size = 254159, upload-time = "2026-03-17T10:30:47.204Z" }, - { url = "https://files.pythonhosted.org/packages/9d/00/7b0edcfe64e2ed4c0340dac14a52ad0f4c9bd0b8b5e531af7d55b703db7c/coverage-7.13.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3f4818d065964db3c1c66dc0fbdac5ac692ecbc875555e13374fdbe7eedb4376", size = 255270, upload-time = "2026-03-17T10:30:48.812Z" }, - { url = "https://files.pythonhosted.org/packages/93/89/7ffc4ba0f5d0a55c1e84ea7cee39c9fc06af7b170513d83fbf3bbefce280/coverage-7.13.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:012d5319e66e9d5a218834642d6c35d265515a62f01157a45bcc036ecf947256", size = 257538, upload-time = "2026-03-17T10:30:50.77Z" }, - { url = "https://files.pythonhosted.org/packages/81/bd/73ddf85f93f7e6fa83e77ccecb6162d9415c79007b4bc124008a4995e4a7/coverage-7.13.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8dd02af98971bdb956363e4827d34425cb3df19ee550ef92855b0acb9c7ce51c", size = 251821, upload-time = "2026-03-17T10:30:52.5Z" }, - { url = "https://files.pythonhosted.org/packages/a0/81/278aff4e8dec4926a0bcb9486320752811f543a3ce5b602cc7a29978d073/coverage-7.13.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f08fd75c50a760c7eb068ae823777268daaf16a80b918fa58eea888f8e3919f5", size = 253191, upload-time = "2026-03-17T10:30:54.543Z" }, - { url = "https://files.pythonhosted.org/packages/70/ee/fe1621488e2e0a58d7e94c4800f0d96f79671553488d401a612bebae324b/coverage-7.13.5-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:843ea8643cf967d1ac7e8ecd4bb00c99135adf4816c0c0593fdcc47b597fcf09", size = 251337, upload-time = "2026-03-17T10:30:56.663Z" }, - { url = "https://files.pythonhosted.org/packages/37/a6/f79fb37aa104b562207cc23cb5711ab6793608e246cae1e93f26b2236ed9/coverage-7.13.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:9d44d7aa963820b1b971dbecd90bfe5fe8f81cff79787eb6cca15750bd2f79b9", size = 255404, upload-time = "2026-03-17T10:30:58.427Z" }, - { url = "https://files.pythonhosted.org/packages/75/f0/ed15262a58ec81ce457ceb717b7f78752a1713556b19081b76e90896e8d4/coverage-7.13.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:7132bed4bd7b836200c591410ae7d97bf7ae8be6fc87d160b2bd881df929e7bf", size = 250903, upload-time = "2026-03-17T10:31:00.093Z" }, - { url = "https://files.pythonhosted.org/packages/0f/e9/9129958f20e7e9d4d56d51d42ccf708d15cac355ff4ac6e736e97a9393d2/coverage-7.13.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a698e363641b98843c517817db75373c83254781426e94ada3197cabbc2c919c", size = 252780, upload-time = "2026-03-17T10:31:01.916Z" }, - { url = "https://files.pythonhosted.org/packages/a4/d7/0ad9b15812d81272db94379fe4c6df8fd17781cc7671fdfa30c76ba5ff7b/coverage-7.13.5-cp312-cp312-win32.whl", hash = "sha256:bdba0a6b8812e8c7df002d908a9a2ea3c36e92611b5708633c50869e6d922fdf", size = 222093, upload-time = "2026-03-17T10:31:03.642Z" }, - { url = "https://files.pythonhosted.org/packages/29/3d/821a9a5799fac2556bcf0bd37a70d1d11fa9e49784b6d22e92e8b2f85f18/coverage-7.13.5-cp312-cp312-win_amd64.whl", hash = "sha256:d2c87e0c473a10bffe991502eac389220533024c8082ec1ce849f4218dded810", size = 222900, upload-time = "2026-03-17T10:31:05.651Z" }, - { url = "https://files.pythonhosted.org/packages/d4/fa/2238c2ad08e35cf4f020ea721f717e09ec3152aea75d191a7faf3ef009a8/coverage-7.13.5-cp312-cp312-win_arm64.whl", hash = "sha256:bf69236a9a81bdca3bff53796237aab096cdbf8d78a66ad61e992d9dac7eb2de", size = 221515, upload-time = "2026-03-17T10:31:07.293Z" }, - { url = "https://files.pythonhosted.org/packages/74/8c/74fedc9663dcf168b0a059d4ea756ecae4da77a489048f94b5f512a8d0b3/coverage-7.13.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5ec4af212df513e399cf11610cc27063f1586419e814755ab362e50a85ea69c1", size = 219576, upload-time = "2026-03-17T10:31:09.045Z" }, - { url = "https://files.pythonhosted.org/packages/0c/c9/44fb661c55062f0818a6ffd2685c67aa30816200d5f2817543717d4b92eb/coverage-7.13.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:941617e518602e2d64942c88ec8499f7fbd49d3f6c4327d3a71d43a1973032f3", size = 219942, upload-time = "2026-03-17T10:31:10.708Z" }, - { url = "https://files.pythonhosted.org/packages/5f/13/93419671cee82b780bab7ea96b67c8ef448f5f295f36bf5031154ec9a790/coverage-7.13.5-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:da305e9937617ee95c2e39d8ff9f040e0487cbf1ac174f777ed5eddd7a7c1f26", size = 250935, upload-time = "2026-03-17T10:31:12.392Z" }, - { url = "https://files.pythonhosted.org/packages/ac/68/1666e3a4462f8202d836920114fa7a5ee9275d1fa45366d336c551a162dd/coverage-7.13.5-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:78e696e1cc714e57e8b25760b33a8b1026b7048d270140d25dafe1b0a1ee05a3", size = 253541, upload-time = "2026-03-17T10:31:14.247Z" }, - { url = "https://files.pythonhosted.org/packages/4e/5e/3ee3b835647be646dcf3c65a7c6c18f87c27326a858f72ab22c12730773d/coverage-7.13.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:02ca0eed225b2ff301c474aeeeae27d26e2537942aa0f87491d3e147e784a82b", size = 254780, upload-time = "2026-03-17T10:31:16.193Z" }, - { url = "https://files.pythonhosted.org/packages/44/b3/cb5bd1a04cfcc49ede6cd8409d80bee17661167686741e041abc7ee1b9a9/coverage-7.13.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:04690832cbea4e4663d9149e05dba142546ca05cb1848816760e7f58285c970a", size = 256912, upload-time = "2026-03-17T10:31:17.89Z" }, - { url = "https://files.pythonhosted.org/packages/1b/66/c1dceb7b9714473800b075f5c8a84f4588f887a90eb8645282031676e242/coverage-7.13.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0590e44dd2745c696a778f7bab6aa95256de2cbc8b8cff4f7db8ff09813d6969", size = 251165, upload-time = "2026-03-17T10:31:19.605Z" }, - { url = "https://files.pythonhosted.org/packages/b7/62/5502b73b97aa2e53ea22a39cf8649ff44827bef76d90bf638777daa27a9d/coverage-7.13.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d7cfad2d6d81dd298ab6b89fe72c3b7b05ec7544bdda3b707ddaecff8d25c161", size = 252908, upload-time = "2026-03-17T10:31:21.312Z" }, - { url = "https://files.pythonhosted.org/packages/7d/37/7792c2d69854397ca77a55c4646e5897c467928b0e27f2d235d83b5d08c6/coverage-7.13.5-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e092b9499de38ae0fbfbc603a74660eb6ff3e869e507b50d85a13b6db9863e15", size = 250873, upload-time = "2026-03-17T10:31:23.565Z" }, - { url = "https://files.pythonhosted.org/packages/a3/23/bc866fb6163be52a8a9e5d708ba0d3b1283c12158cefca0a8bbb6e247a43/coverage-7.13.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:48c39bc4a04d983a54a705a6389512883d4a3b9862991b3617d547940e9f52b1", size = 255030, upload-time = "2026-03-17T10:31:25.58Z" }, - { url = "https://files.pythonhosted.org/packages/7d/8b/ef67e1c222ef49860701d346b8bbb70881bef283bd5f6cbba68a39a086c7/coverage-7.13.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2d3807015f138ffea1ed9afeeb8624fd781703f2858b62a8dd8da5a0994c57b6", size = 250694, upload-time = "2026-03-17T10:31:27.316Z" }, - { url = "https://files.pythonhosted.org/packages/46/0d/866d1f74f0acddbb906db212e096dee77a8e2158ca5e6bb44729f9d93298/coverage-7.13.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ee2aa19e03161671ec964004fb74b2257805d9710bf14a5c704558b9d8dbaf17", size = 252469, upload-time = "2026-03-17T10:31:29.472Z" }, - { url = "https://files.pythonhosted.org/packages/7a/f5/be742fec31118f02ce42b21c6af187ad6a344fed546b56ca60caacc6a9a0/coverage-7.13.5-cp313-cp313-win32.whl", hash = "sha256:ce1998c0483007608c8382f4ff50164bfc5bd07a2246dd272aa4043b75e61e85", size = 222112, upload-time = "2026-03-17T10:31:31.526Z" }, - { url = "https://files.pythonhosted.org/packages/66/40/7732d648ab9d069a46e686043241f01206348e2bbf128daea85be4d6414b/coverage-7.13.5-cp313-cp313-win_amd64.whl", hash = "sha256:631efb83f01569670a5e866ceb80fe483e7c159fac6f167e6571522636104a0b", size = 222923, upload-time = "2026-03-17T10:31:33.633Z" }, - { url = "https://files.pythonhosted.org/packages/48/af/fea819c12a095781f6ccd504890aaddaf88b8fab263c4940e82c7b770124/coverage-7.13.5-cp313-cp313-win_arm64.whl", hash = "sha256:f4cd16206ad171cbc2470dbea9103cf9a7607d5fe8c242fdf1edf36174020664", size = 221540, upload-time = "2026-03-17T10:31:35.445Z" }, - { url = "https://files.pythonhosted.org/packages/23/d2/17879af479df7fbbd44bd528a31692a48f6b25055d16482fdf5cdb633805/coverage-7.13.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0428cbef5783ad91fe240f673cc1f76b25e74bbfe1a13115e4aa30d3f538162d", size = 220262, upload-time = "2026-03-17T10:31:37.184Z" }, - { url = "https://files.pythonhosted.org/packages/5b/4c/d20e554f988c8f91d6a02c5118f9abbbf73a8768a3048cb4962230d5743f/coverage-7.13.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e0b216a19534b2427cc201a26c25da4a48633f29a487c61258643e89d28200c0", size = 220617, upload-time = "2026-03-17T10:31:39.245Z" }, - { url = "https://files.pythonhosted.org/packages/29/9c/f9f5277b95184f764b24e7231e166dfdb5780a46d408a2ac665969416d61/coverage-7.13.5-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:972a9cd27894afe4bc2b1480107054e062df08e671df7c2f18c205e805ccd806", size = 261912, upload-time = "2026-03-17T10:31:41.324Z" }, - { url = "https://files.pythonhosted.org/packages/d5/f6/7f1ab39393eeb50cfe4747ae8ef0e4fc564b989225aa1152e13a180d74f8/coverage-7.13.5-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4b59148601efcd2bac8c4dbf1f0ad6391693ccf7a74b8205781751637076aee3", size = 263987, upload-time = "2026-03-17T10:31:43.724Z" }, - { url = "https://files.pythonhosted.org/packages/a0/d7/62c084fb489ed9c6fbdf57e006752e7c516ea46fd690e5ed8b8617c7d52e/coverage-7.13.5-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:505d7083c8b0c87a8fa8c07370c285847c1f77739b22e299ad75a6af6c32c5c9", size = 266416, upload-time = "2026-03-17T10:31:45.769Z" }, - { url = "https://files.pythonhosted.org/packages/a9/f6/df63d8660e1a0bff6125947afda112a0502736f470d62ca68b288ea762d8/coverage-7.13.5-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:60365289c3741e4db327e7baff2a4aaacf22f788e80fa4683393891b70a89fbd", size = 267558, upload-time = "2026-03-17T10:31:48.293Z" }, - { url = "https://files.pythonhosted.org/packages/5b/02/353ca81d36779bd108f6d384425f7139ac3c58c750dcfaafe5d0bee6436b/coverage-7.13.5-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1b88c69c8ef5d4b6fe7dea66d6636056a0f6a7527c440e890cf9259011f5e606", size = 261163, upload-time = "2026-03-17T10:31:50.125Z" }, - { url = "https://files.pythonhosted.org/packages/2c/16/2e79106d5749bcaf3aee6d309123548e3276517cd7851faa8da213bc61bf/coverage-7.13.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5b13955d31d1633cf9376908089b7cebe7d15ddad7aeaabcbe969a595a97e95e", size = 263981, upload-time = "2026-03-17T10:31:51.961Z" }, - { url = "https://files.pythonhosted.org/packages/29/c7/c29e0c59ffa6942030ae6f50b88ae49988e7e8da06de7ecdbf49c6d4feae/coverage-7.13.5-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:f70c9ab2595c56f81a89620e22899eea8b212a4041bd728ac6f4a28bf5d3ddd0", size = 261604, upload-time = "2026-03-17T10:31:53.872Z" }, - { url = "https://files.pythonhosted.org/packages/40/48/097cdc3db342f34006a308ab41c3a7c11c3f0d84750d340f45d88a782e00/coverage-7.13.5-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:084b84a8c63e8d6fc7e3931b316a9bcafca1458d753c539db82d31ed20091a87", size = 265321, upload-time = "2026-03-17T10:31:55.997Z" }, - { url = "https://files.pythonhosted.org/packages/bb/1f/4994af354689e14fd03a75f8ec85a9a68d94e0188bbdab3fc1516b55e512/coverage-7.13.5-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ad14385487393e386e2ea988b09d62dd42c397662ac2dabc3832d71253eee479", size = 260502, upload-time = "2026-03-17T10:31:58.308Z" }, - { url = "https://files.pythonhosted.org/packages/22/c6/9bb9ef55903e628033560885f5c31aa227e46878118b63ab15dc7ba87797/coverage-7.13.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:7f2c47b36fe7709a6e83bfadf4eefb90bd25fbe4014d715224c4316f808e59a2", size = 262688, upload-time = "2026-03-17T10:32:00.141Z" }, - { url = "https://files.pythonhosted.org/packages/14/4f/f5df9007e50b15e53e01edea486814783a7f019893733d9e4d6caad75557/coverage-7.13.5-cp313-cp313t-win32.whl", hash = "sha256:67e9bc5449801fad0e5dff329499fb090ba4c5800b86805c80617b4e29809b2a", size = 222788, upload-time = "2026-03-17T10:32:02.246Z" }, - { url = "https://files.pythonhosted.org/packages/e1/98/aa7fccaa97d0f3192bec013c4e6fd6d294a6ed44b640e6bb61f479e00ed5/coverage-7.13.5-cp313-cp313t-win_amd64.whl", hash = "sha256:da86cdcf10d2519e10cabb8ac2de03da1bcb6e4853790b7fbd48523332e3a819", size = 223851, upload-time = "2026-03-17T10:32:04.416Z" }, - { url = "https://files.pythonhosted.org/packages/3d/8b/e5c469f7352651e5f013198e9e21f97510b23de957dd06a84071683b4b60/coverage-7.13.5-cp313-cp313t-win_arm64.whl", hash = "sha256:0ecf12ecb326fe2c339d93fc131816f3a7367d223db37817208905c89bded911", size = 222104, upload-time = "2026-03-17T10:32:06.65Z" }, - { url = "https://files.pythonhosted.org/packages/8e/77/39703f0d1d4b478bfd30191d3c14f53caf596fac00efb3f8f6ee23646439/coverage-7.13.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fbabfaceaeb587e16f7008f7795cd80d20ec548dc7f94fbb0d4ec2e038ce563f", size = 219621, upload-time = "2026-03-17T10:32:08.589Z" }, - { url = "https://files.pythonhosted.org/packages/e2/3e/51dff36d99ae14639a133d9b164d63e628532e2974d8b1edb99dd1ebc733/coverage-7.13.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9bb2a28101a443669a423b665939381084412b81c3f8c0fcfbac57f4e30b5b8e", size = 219953, upload-time = "2026-03-17T10:32:10.507Z" }, - { url = "https://files.pythonhosted.org/packages/6a/6c/1f1917b01eb647c2f2adc9962bd66c79eb978951cab61bdc1acab3290c07/coverage-7.13.5-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bd3a2fbc1c6cccb3c5106140d87cc6a8715110373ef42b63cf5aea29df8c217a", size = 250992, upload-time = "2026-03-17T10:32:12.41Z" }, - { url = "https://files.pythonhosted.org/packages/22/e5/06b1f88f42a5a99df42ce61208bdec3bddb3d261412874280a19796fc09c/coverage-7.13.5-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6c36ddb64ed9d7e496028d1d00dfec3e428e0aabf4006583bb1839958d280510", size = 253503, upload-time = "2026-03-17T10:32:14.449Z" }, - { url = "https://files.pythonhosted.org/packages/80/28/2a148a51e5907e504fa7b85490277734e6771d8844ebcc48764a15e28155/coverage-7.13.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:380e8e9084d8eb38db3a9176a1a4f3c0082c3806fa0dc882d1d87abc3c789247", size = 254852, upload-time = "2026-03-17T10:32:16.56Z" }, - { url = "https://files.pythonhosted.org/packages/61/77/50e8d3d85cc0b7ebe09f30f151d670e302c7ff4a1bf6243f71dd8b0981fa/coverage-7.13.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e808af52a0513762df4d945ea164a24b37f2f518cbe97e03deaa0ee66139b4d6", size = 257161, upload-time = "2026-03-17T10:32:19.004Z" }, - { url = "https://files.pythonhosted.org/packages/3b/c4/b5fd1d4b7bf8d0e75d997afd3925c59ba629fc8616f1b3aae7605132e256/coverage-7.13.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e301d30dd7e95ae068671d746ba8c34e945a82682e62918e41b2679acd2051a0", size = 251021, upload-time = "2026-03-17T10:32:21.344Z" }, - { url = "https://files.pythonhosted.org/packages/f8/66/6ea21f910e92d69ef0b1c3346ea5922a51bad4446c9126db2ae96ee24c4c/coverage-7.13.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:800bc829053c80d240a687ceeb927a94fd108bbdc68dfbe505d0d75ab578a882", size = 252858, upload-time = "2026-03-17T10:32:23.506Z" }, - { url = "https://files.pythonhosted.org/packages/9e/ea/879c83cb5d61aa2a35fb80e72715e92672daef8191b84911a643f533840c/coverage-7.13.5-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:0b67af5492adb31940ee418a5a655c28e48165da5afab8c7fa6fd72a142f8740", size = 250823, upload-time = "2026-03-17T10:32:25.516Z" }, - { url = "https://files.pythonhosted.org/packages/8a/fb/616d95d3adb88b9803b275580bdeee8bd1b69a886d057652521f83d7322f/coverage-7.13.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c9136ff29c3a91e25b1d1552b5308e53a1e0653a23e53b6366d7c2dcbbaf8a16", size = 255099, upload-time = "2026-03-17T10:32:27.944Z" }, - { url = "https://files.pythonhosted.org/packages/1c/93/25e6917c90ec1c9a56b0b26f6cad6408e5f13bb6b35d484a0d75c9cf000d/coverage-7.13.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:cff784eef7f0b8f6cb28804fbddcfa99f89efe4cc35fb5627e3ac58f91ed3ac0", size = 250638, upload-time = "2026-03-17T10:32:29.914Z" }, - { url = "https://files.pythonhosted.org/packages/fc/7b/dc1776b0464145a929deed214aef9fb1493f159b59ff3c7eeeedf91eddd0/coverage-7.13.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:68a4953be99b17ac3c23b6efbc8a38330d99680c9458927491d18700ef23ded0", size = 252295, upload-time = "2026-03-17T10:32:31.981Z" }, - { url = "https://files.pythonhosted.org/packages/ea/fb/99cbbc56a26e07762a2740713f3c8f9f3f3106e3a3dd8cc4474954bccd34/coverage-7.13.5-cp314-cp314-win32.whl", hash = "sha256:35a31f2b1578185fbe6aa2e74cea1b1d0bbf4c552774247d9160d29b80ed56cc", size = 222360, upload-time = "2026-03-17T10:32:34.233Z" }, - { url = "https://files.pythonhosted.org/packages/8d/b7/4758d4f73fb536347cc5e4ad63662f9d60ba9118cb6785e9616b2ce5d7fa/coverage-7.13.5-cp314-cp314-win_amd64.whl", hash = "sha256:2aa055ae1857258f9e0045be26a6d62bdb47a72448b62d7b55f4820f361a2633", size = 223174, upload-time = "2026-03-17T10:32:36.369Z" }, - { url = "https://files.pythonhosted.org/packages/2c/f2/24d84e1dfe70f8ac9fdf30d338239860d0d1d5da0bda528959d0ebc9da28/coverage-7.13.5-cp314-cp314-win_arm64.whl", hash = "sha256:1b11eef33edeae9d142f9b4358edb76273b3bfd30bc3df9a4f95d0e49caf94e8", size = 221739, upload-time = "2026-03-17T10:32:38.736Z" }, - { url = "https://files.pythonhosted.org/packages/60/5b/4a168591057b3668c2428bff25dd3ebc21b629d666d90bcdfa0217940e84/coverage-7.13.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:10a0c37f0b646eaff7cce1874c31d1f1ccb297688d4c747291f4f4c70741cc8b", size = 220351, upload-time = "2026-03-17T10:32:41.196Z" }, - { url = "https://files.pythonhosted.org/packages/f5/21/1fd5c4dbfe4a58b6b99649125635df46decdfd4a784c3cd6d410d303e370/coverage-7.13.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b5db73ba3c41c7008037fa731ad5459fc3944cb7452fc0aa9f822ad3533c583c", size = 220612, upload-time = "2026-03-17T10:32:43.204Z" }, - { url = "https://files.pythonhosted.org/packages/d6/fe/2a924b3055a5e7e4512655a9d4609781b0d62334fa0140c3e742926834e2/coverage-7.13.5-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:750db93a81e3e5a9831b534be7b1229df848b2e125a604fe6651e48aa070e5f9", size = 261985, upload-time = "2026-03-17T10:32:45.514Z" }, - { url = "https://files.pythonhosted.org/packages/d7/0d/c8928f2bd518c45990fe1a2ab8db42e914ef9b726c975facc4282578c3eb/coverage-7.13.5-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9ddb4f4a5479f2539644be484da179b653273bca1a323947d48ab107b3ed1f29", size = 264107, upload-time = "2026-03-17T10:32:47.971Z" }, - { url = "https://files.pythonhosted.org/packages/ef/ae/4ae35bbd9a0af9d820362751f0766582833c211224b38665c0f8de3d487f/coverage-7.13.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8a7a2049c14f413163e2bdabd37e41179b1d1ccb10ffc6ccc4b7a718429c607", size = 266513, upload-time = "2026-03-17T10:32:50.1Z" }, - { url = "https://files.pythonhosted.org/packages/9c/20/d326174c55af36f74eac6ae781612d9492f060ce8244b570bb9d50d9d609/coverage-7.13.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1c85e0b6c05c592ea6d8768a66a254bfb3874b53774b12d4c89c481eb78cb90", size = 267650, upload-time = "2026-03-17T10:32:52.391Z" }, - { url = "https://files.pythonhosted.org/packages/7a/5e/31484d62cbd0eabd3412e30d74386ece4a0837d4f6c3040a653878bfc019/coverage-7.13.5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:777c4d1eff1b67876139d24288aaf1817f6c03d6bae9c5cc8d27b83bcfe38fe3", size = 261089, upload-time = "2026-03-17T10:32:54.544Z" }, - { url = "https://files.pythonhosted.org/packages/e9/d8/49a72d6de146eebb0b7e48cc0f4bc2c0dd858e3d4790ab2b39a2872b62bd/coverage-7.13.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6697e29b93707167687543480a40f0db8f356e86d9f67ddf2e37e2dfd91a9dab", size = 263982, upload-time = "2026-03-17T10:32:56.803Z" }, - { url = "https://files.pythonhosted.org/packages/06/3b/0351f1bd566e6e4dd39e978efe7958bde1d32f879e85589de147654f57bb/coverage-7.13.5-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:8fdf453a942c3e4d99bd80088141c4c6960bb232c409d9c3558e2dbaa3998562", size = 261579, upload-time = "2026-03-17T10:32:59.466Z" }, - { url = "https://files.pythonhosted.org/packages/5d/ce/796a2a2f4017f554d7810f5c573449b35b1e46788424a548d4d19201b222/coverage-7.13.5-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:32ca0c0114c9834a43f045a87dcebd69d108d8ffb666957ea65aa132f50332e2", size = 265316, upload-time = "2026-03-17T10:33:01.847Z" }, - { url = "https://files.pythonhosted.org/packages/3d/16/d5ae91455541d1a78bc90abf495be600588aff8f6db5c8b0dae739fa39c9/coverage-7.13.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:8769751c10f339021e2638cd354e13adeac54004d1941119b2c96fe5276d45ea", size = 260427, upload-time = "2026-03-17T10:33:03.945Z" }, - { url = "https://files.pythonhosted.org/packages/48/11/07f413dba62db21fb3fad5d0de013a50e073cc4e2dc4306e770360f6dfc8/coverage-7.13.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cec2d83125531bd153175354055cdb7a09987af08a9430bd173c937c6d0fba2a", size = 262745, upload-time = "2026-03-17T10:33:06.285Z" }, - { url = "https://files.pythonhosted.org/packages/91/15/d792371332eb4663115becf4bad47e047d16234b1aff687b1b18c58d60ae/coverage-7.13.5-cp314-cp314t-win32.whl", hash = "sha256:0cd9ed7a8b181775459296e402ca4fb27db1279740a24e93b3b41942ebe4b215", size = 223146, upload-time = "2026-03-17T10:33:08.756Z" }, - { url = "https://files.pythonhosted.org/packages/db/51/37221f59a111dca5e85be7dbf09696323b5b9f13ff65e0641d535ed06ea8/coverage-7.13.5-cp314-cp314t-win_amd64.whl", hash = "sha256:301e3b7dfefecaca37c9f1aa6f0049b7d4ab8dd933742b607765d757aca77d43", size = 224254, upload-time = "2026-03-17T10:33:11.174Z" }, - { url = "https://files.pythonhosted.org/packages/54/83/6acacc889de8987441aa7d5adfbdbf33d288dad28704a67e574f1df9bcbb/coverage-7.13.5-cp314-cp314t-win_arm64.whl", hash = "sha256:9dacc2ad679b292709e0f5fc1ac74a6d4d5562e424058962c7bb0c658ad25e45", size = 222276, upload-time = "2026-03-17T10:33:13.466Z" }, - { url = "https://files.pythonhosted.org/packages/9e/ee/a4cf96b8ce1e566ed238f0659ac2d3f007ed1d14b181bcb684e19561a69a/coverage-7.13.5-py3-none-any.whl", hash = "sha256:34b02417cf070e173989b3db962f7ed56d2f644307b2cf9d5a0f258e13084a61", size = 211346, upload-time = "2026-03-17T10:33:15.691Z" }, + { url = "https://files.pythonhosted.org/packages/10/03/060ce69008ac97bbc01b1411b3e55b61f6f015659400b46749b662107831/coverage-7.15.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9b5bd92ff1ec22e535eab0de75fa6db021992791f461a2aceb7822c625a1187d", size = 221284, upload-time = "2026-07-15T18:53:29.52Z" }, + { url = "https://files.pythonhosted.org/packages/fc/a3/d936e8b53edd9684100a6aefaf3fcabaa54728fe33324436c8d279c047aa/coverage-7.15.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:44826758cfe73fcd0e6af5deb4ba6d5417cc1d13df3acb35c93484a11160f846", size = 221799, upload-time = "2026-07-15T18:53:31.708Z" }, + { url = "https://files.pythonhosted.org/packages/ae/a3/ca234b06aec7ee28226f11d39a696b4481fe5eddfce8e03bf39979bb8ffb/coverage-7.15.2-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:09f5c6ec5901f667bd97dd140b5b9a2586b10efec66f46fb1e6d8135f8b95bdf", size = 248544, upload-time = "2026-07-15T18:53:33.212Z" }, + { url = "https://files.pythonhosted.org/packages/2b/89/dda79527bb7573ba91828b2fb91b3105d87378d6a2749ca0c0924ce0addd/coverage-7.15.2-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1d16e3a7104ea84f03e614611b3edbf6fb6892554b3ab0fe7fbb3f2b2ef04376", size = 250374, upload-time = "2026-07-15T18:53:34.683Z" }, + { url = "https://files.pythonhosted.org/packages/67/c6/c33755a34572f81f49a8c0cdf6b622f35ccb3238b136e1909daf0cdd4319/coverage-7.15.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d46e62cb35d91e6e2589fda6d28074426b0e276422b5d2ebef2c6b11dc60dbfd", size = 252239, upload-time = "2026-07-15T18:53:36.205Z" }, + { url = "https://files.pythonhosted.org/packages/b9/6f/dc341741b375be53a5baeee5b4bf0f0e525d38caed428f7932d23bb7bcb1/coverage-7.15.2-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:dfd3db045e95960ae3683059571e597fda7cc610106a8916f77c5839048c1deb", size = 254150, upload-time = "2026-07-15T18:53:37.863Z" }, + { url = "https://files.pythonhosted.org/packages/e9/8d/966a18a5b195cb4e77b14c53f5f3dce22b5da05e6de7fafd1e08f2d2067a/coverage-7.15.2-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:affd532502d34c0472d0cdb181325c89f1d2c44992fef0c17e88e7b1576259a1", size = 249234, upload-time = "2026-07-15T18:53:39.394Z" }, + { url = "https://files.pythonhosted.org/packages/c5/8b/8b2e367496ab48484d48e79984fec76cdc1b7cb5d3a00ee799a5602e3ec9/coverage-7.15.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d17d7512151fedfcc64c1821a8977fc9be0dbf495754669afcab7b57abc98ae9", size = 250276, upload-time = "2026-07-15T18:53:41.027Z" }, + { url = "https://files.pythonhosted.org/packages/63/92/1199318a200eb6c8c6ce0192c892c8710ac791abbe0f35099294620bbfda/coverage-7.15.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e26ff680768b8095e8874aabe0e9d3a47a2a9f176a8340d05f8604c56457c23a", size = 248283, upload-time = "2026-07-15T18:53:42.557Z" }, + { url = "https://files.pythonhosted.org/packages/56/da/be284a55c5619bda891a89c27dfd59324a2c6a14d755cf6aac6960ceebeb/coverage-7.15.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:7e8f27131dc7cd53de2c137dd207b3720919320b3c20d499dc30aa9ee6173287", size = 252093, upload-time = "2026-07-15T18:53:44.271Z" }, + { url = "https://files.pythonhosted.org/packages/d4/53/ee112da833ddd77b73c6d781a98029b45b584b136615b4900ed0569f887e/coverage-7.15.2-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:728a33676d4c3f0db977990a4bd421dcaa3be3e53b5b6273036fff6666008e89", size = 248552, upload-time = "2026-07-15T18:53:45.7Z" }, + { url = "https://files.pythonhosted.org/packages/82/6a/802cfc802e9113494c80bf3f284cd4d72faeb1f24e244f61046af364f2ca/coverage-7.15.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:29c052f7c83ccfcc5c577eaae025d2e4a9bb80daf03c0ac31c996e83b000ce88", size = 249154, upload-time = "2026-07-15T18:53:47.256Z" }, + { url = "https://files.pythonhosted.org/packages/2c/65/529808e91d651147edae408fd9e894abc3b8cad7f3e594bbc36719a3e13a/coverage-7.15.2-cp310-cp310-win32.whl", hash = "sha256:1268ac8fb9ddcd783d3948dbabaf80a5d53bfdaa0575e873e2139a692f797443", size = 223334, upload-time = "2026-07-15T18:53:48.768Z" }, + { url = "https://files.pythonhosted.org/packages/68/0f/0e1829d7001130876dfbc0b4e1c737ea7c155b809e3e4a98a0aa268e2369/coverage-7.15.2-cp310-cp310-win_amd64.whl", hash = "sha256:9f4432898c4bf2fba0435bbe35dd4437d7264565e5a88a21f5b49d8662a6b629", size = 223959, upload-time = "2026-07-15T18:53:50.429Z" }, + { url = "https://files.pythonhosted.org/packages/7d/3a/54536704f507d4573bf9161c4d0dd3dd59b6d85e48c664e901b6844d8e33/coverage-7.15.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2f1ec6f304b156669cfde653b4e9a953f5de87e247ea02ac599bce0ab2744036", size = 221414, upload-time = "2026-07-15T18:53:51.941Z" }, + { url = "https://files.pythonhosted.org/packages/b6/d9/8ba925d29743e3577b21e4d8c11a702b76bc93c41e7fdfd1177af63d4b8d/coverage-7.15.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4d3361879d736f469f45723c11ea1a5bbdaf1f6928f0e632c940378b5aa9b660", size = 221913, upload-time = "2026-07-15T18:53:53.682Z" }, + { url = "https://files.pythonhosted.org/packages/09/54/a855f3aa0187f2b431ade4e4791b77b56282cfb5d201c83ec26a31b5b36a/coverage-7.15.2-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c6a98d698f9e2c8008d0370ec7fc452ebfcc530002ae2d0061170d768b992589", size = 252332, upload-time = "2026-07-15T18:53:55.467Z" }, + { url = "https://files.pythonhosted.org/packages/8e/d3/13ac97b4370640ba3452fc8559b06cc2f479ce3ba4a0b632a73e44c38a7d/coverage-7.15.2-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d50dd325e18ec25bfcc10cd7f99b04df1ab9ec76b0918c260e60817ad0643dee", size = 254243, upload-time = "2026-07-15T18:53:57.055Z" }, + { url = "https://files.pythonhosted.org/packages/88/83/5eca144942d8d0659d3f55176517f4a59cdc65eefd17146a0770935a3ebd/coverage-7.15.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:67d7602480a47bdf5b675635403625553ebaa70d5a62a657c035149fd401cea0", size = 256352, upload-time = "2026-07-15T18:53:58.83Z" }, + { url = "https://files.pythonhosted.org/packages/4e/ba/d3db2e01a50fc88cdb4c0f19542bcf6f61489e34dc9aa3538413e2459a38/coverage-7.15.2-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cee0f89f4767a6057c8fbf168f8135f18be651300496086bd873e3189fed0487", size = 258313, upload-time = "2026-07-15T18:54:00.497Z" }, + { url = "https://files.pythonhosted.org/packages/78/b3/aba83416e9177df28e5186d856c19158c59fc0e7e814aaa61a4a2354ad1b/coverage-7.15.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a29ec5305a7335aacee2d799e3422e91e1c8a12474986e2b3b07e315c91be82f", size = 252449, upload-time = "2026-07-15T18:54:02.456Z" }, + { url = "https://files.pythonhosted.org/packages/6a/a5/4b00ecac0194431ab451b0f6710f8e2517d04cef60f821b14dec4637d575/coverage-7.15.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:48ccc6395958eda89093ecdc35644c86f23a8b23a7f4d44958812b721aad67c1", size = 254043, upload-time = "2026-07-15T18:54:04.072Z" }, + { url = "https://files.pythonhosted.org/packages/75/b6/cfa209b4313ee7f1b34da47efcd789ea51c024ad35af390e00f5a3c10a2e/coverage-7.15.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:81f382c5a94b434ec1f6da607edb904c76d7212e618cd4d1bc9f97bed4120ef5", size = 252107, upload-time = "2026-07-15T18:54:06.745Z" }, + { url = "https://files.pythonhosted.org/packages/36/67/e8cac5a6954038c98d7fe7eb9802afe7ab3ecb637bb7cc00e69b4148b56d/coverage-7.15.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:bbc808daf4f5cd567af8075ecc72d21c6dfef9a254709a621a84c217c935ebc0", size = 255873, upload-time = "2026-07-15T18:54:08.48Z" }, + { url = "https://files.pythonhosted.org/packages/2c/92/395cca9f330a86c3fe3471d73e2c102116c4c58fdc619dbbc125c6e93a54/coverage-7.15.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:a4c46b247b5d4b78f613bd89fea926d32b25c6cc61a50bd1e99ba310348f3dad", size = 251826, upload-time = "2026-07-15T18:54:10.083Z" }, + { url = "https://files.pythonhosted.org/packages/51/60/3e91b20295439652424f426b7086ec5bf4fbe3f604c73eda22b986c4fd6b/coverage-7.15.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:094dd37f3ef7b2da8b068b583d1f4c40f91c65197e16c52a71962d5d537fc5db", size = 252735, upload-time = "2026-07-15T18:54:11.878Z" }, + { url = "https://files.pythonhosted.org/packages/a5/eb/8c07839005e5e3c6b3877d3a6e2a80ce766589f31dd2b6882b78d59a7b8c/coverage-7.15.2-cp311-cp311-win32.whl", hash = "sha256:a63b9e190711134d581c4d703df5df09851b1acf99792c7aacbbe9f41f0283c9", size = 223500, upload-time = "2026-07-15T18:54:13.525Z" }, + { url = "https://files.pythonhosted.org/packages/2e/98/59d83c257cd59f0fbaf9d9ddb26b744a576760dfd1ae16e516408894a02b/coverage-7.15.2-cp311-cp311-win_amd64.whl", hash = "sha256:8bb9f4b4279187560796a4cdaca3b0a93dd97e48ee667df005f4ed9a97403688", size = 223973, upload-time = "2026-07-15T18:54:15.163Z" }, + { url = "https://files.pythonhosted.org/packages/ea/09/2d285c8bef5c4f695d120c1c96dc11715638aa8e134069f210bb6a62a9fe/coverage-7.15.2-cp311-cp311-win_arm64.whl", hash = "sha256:8c726b232659cbd2ae57ade46509eb068c9bd7a06df9fcbff6fe484870006934", size = 223519, upload-time = "2026-07-15T18:54:16.803Z" }, + { url = "https://files.pythonhosted.org/packages/6a/50/eb5bf42e531611a9f8d272556b1ed4de503f84a91413584094487cf69f8f/coverage-7.15.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:1adac78e5abc7c5438f7a209c9ca69d06542f0bf481d728b6989ea80b813fdf9", size = 221587, upload-time = "2026-07-15T18:54:18.439Z" }, + { url = "https://files.pythonhosted.org/packages/06/d1/da99af464c335d4e023a6efcd7ec30f63b88a43c93745154ab74ffb31cea/coverage-7.15.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b868acc62aa5de3be7a9d05c2333bf8359ca987e43f9cb30ff8fbda6a024ab73", size = 221943, upload-time = "2026-07-15T18:54:20.062Z" }, + { url = "https://files.pythonhosted.org/packages/5b/8a/13c42723d61ca447eafa18732e8141dd6a63f2732e1c7e1502c182dd88d7/coverage-7.15.2-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6f6966fc30e6f06ca8f98fb0ce51eda6b111b3ee8d066a8b1ec9e77fa06ab55d", size = 253450, upload-time = "2026-07-15T18:54:21.765Z" }, + { url = "https://files.pythonhosted.org/packages/d7/29/99021303f98fbdcb63504b4d07bea4cc025b9b2dd907c4f07c85d50a0dab/coverage-7.15.2-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:68af907f595ab01a78f794932ff3bdf929c316d3000810d38dbc247129e26f8b", size = 256187, upload-time = "2026-07-15T18:54:23.4Z" }, + { url = "https://files.pythonhosted.org/packages/f9/a8/fd503715ed6ca9c5d742923aa5209257340b367a867b2ced0c7d4ba8a0b9/coverage-7.15.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:afa29e2eff3d5729267e2cb2fd4ce9d61c952932fb2694e34ccb5d9540c6a296", size = 257301, upload-time = "2026-07-15T18:54:25.183Z" }, + { url = "https://files.pythonhosted.org/packages/da/40/3f4b8fb409810036ebc2857d36adc0498c6e957b5df0290c5036b2e143f1/coverage-7.15.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bbf44513ceb1589e31948e20eafbde9deaface90e1a1afa5f5f77b4423d17ce6", size = 259562, upload-time = "2026-07-15T18:54:27.204Z" }, + { url = "https://files.pythonhosted.org/packages/0b/8a/9bdffbef47db77cce3d6b02a28f7e919b19f0106c4b080c2c2246040f885/coverage-7.15.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9deddf09eecb717b7f980414b43d90a5b22ff3967d2949ab29cb0aa83d9e9098", size = 253841, upload-time = "2026-07-15T18:54:29.134Z" }, + { url = "https://files.pythonhosted.org/packages/1b/1e/9031efde019d31a06646261fce6dfc5c3c74e951e27a71e5c9a424563178/coverage-7.15.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ae901f7e55ba405c84ee1cab3d3e962e4e871e4a2bcb9c90911adbd69b42ac5a", size = 255221, upload-time = "2026-07-15T18:54:31.142Z" }, + { url = "https://files.pythonhosted.org/packages/56/db/787acde872389fc84a9ef9d8cd1ccc658e391ab4cb5b28092a714426a394/coverage-7.15.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a0f47002c6eeb7c280228467a4cb0cc15ca2103a8421b986b2d3ec04a0f9bd8b", size = 253366, upload-time = "2026-07-15T18:54:32.886Z" }, + { url = "https://files.pythonhosted.org/packages/2f/9b/6f57bc4b93c842eef1695f8cdaf2318e35e7ba54f5ba80d84be213ab7858/coverage-7.15.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1cd7a5beb7af3e864a13b1f0fb26efd3695da43ef0daf71e586adfffaf34d5b2", size = 257434, upload-time = "2026-07-15T18:54:34.7Z" }, + { url = "https://files.pythonhosted.org/packages/88/26/b3186a21b2acc83e451118978905c81c7072c3333707804db09a78c096a2/coverage-7.15.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:97a5c5457a9fb1d6c4e06cfb5dc835871fbfb6a6a51addc9e925bdeff5ef7440", size = 252935, upload-time = "2026-07-15T18:54:36.548Z" }, + { url = "https://files.pythonhosted.org/packages/20/c2/c9f3376b2e717ea69ed7a6e9a5fcab968fb0b290db6cf4bd9a1fc7541b75/coverage-7.15.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0901cfe6c13bcd2302da4f83e884555d2a22bda6e4c476f09ef204ba20ca536e", size = 254807, upload-time = "2026-07-15T18:54:38.296Z" }, + { url = "https://files.pythonhosted.org/packages/f0/e1/dfc15401f4a8aaeb486e1ba3e9e3c40522a6e38bd0ecf0b3f29cb8082957/coverage-7.15.2-cp312-cp312-win32.whl", hash = "sha256:b171bdd71cb7ff792bf32e376173b0ace7e7963e7e57c58dfc42063a6a7174cd", size = 223641, upload-time = "2026-07-15T18:54:40.103Z" }, + { url = "https://files.pythonhosted.org/packages/91/40/81b6d809d320cd366ec5bdf8176575e897dcb8efe7fb4b489ef9e93e4d13/coverage-7.15.2-cp312-cp312-win_amd64.whl", hash = "sha256:582edc45c2040543fef83341be23c43024a3ab3ae0c2d8bc498a06282905ad40", size = 224172, upload-time = "2026-07-15T18:54:41.882Z" }, + { url = "https://files.pythonhosted.org/packages/ef/28/9f14ec438149f7de557f45518f09b4a7917b795cc37083aa7db482693f8c/coverage-7.15.2-cp312-cp312-win_arm64.whl", hash = "sha256:a638db90c61cd219aeee65e83a24fdaa57269a741ae0cf773309208ac862cee3", size = 223556, upload-time = "2026-07-15T18:54:43.674Z" }, + { url = "https://files.pythonhosted.org/packages/fc/d5/f8c838e6b7282976f7c918884b792df7a0c42c5bba5d99c60ad2d221d56d/coverage-7.15.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1121caa19159a38b5463eaae4b1e1fde81e525b15ecc5e000cd5b1a108f743a8", size = 221606, upload-time = "2026-07-15T18:54:45.448Z" }, + { url = "https://files.pythonhosted.org/packages/bf/37/97c926376364f66298cc44893b89cdf17b8bc406376497c4061ae4b8a8ff/coverage-7.15.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a300c6934e0989c327b9e8a1e110329da4641149f872bbe9f70168be66da76c1", size = 221982, upload-time = "2026-07-15T18:54:47.341Z" }, + { url = "https://files.pythonhosted.org/packages/b7/30/a36050a6e83c2135ee0776f452ca3948224befc6d7f26acecc082d0c106a/coverage-7.15.2-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2617f8799d268fabdeef42a7e89ac3a23e1deee9025427db2df970f99a89a578", size = 252972, upload-time = "2026-07-15T18:54:49.2Z" }, + { url = "https://files.pythonhosted.org/packages/31/d3/06b5f1daf95f0f15ab05bd75f26ba5f3c8b33d0bb72f3aaa3cf41d1bad3a/coverage-7.15.2-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7dc2950a2992cd676d35c20ae63522836deeb034f08874699d14068710af3dc1", size = 255569, upload-time = "2026-07-15T18:54:51.098Z" }, + { url = "https://files.pythonhosted.org/packages/81/1c/9afb3f8de2b8d36960391c48559a2e3ff96594b58099f115921549ea8d0d/coverage-7.15.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9e36686f7a442185db2400b3df171aac520869faf9deb59df687d28659eda2a6", size = 256806, upload-time = "2026-07-15T18:54:53.145Z" }, + { url = "https://files.pythonhosted.org/packages/64/d8/b989f96061a5e32d82fddd1b1b9ff48a7c8f8ae7606f0e80fd9de54b1e33/coverage-7.15.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7d29ca7bd67af6e12e74632d65f026eabc1364da5c254494cd914446a28a3ef7", size = 258936, upload-time = "2026-07-15T18:54:55.015Z" }, + { url = "https://files.pythonhosted.org/packages/b8/fa/f99771f5110457c7b511c1935ca49ddf288218eaa84322e028b9334146ae/coverage-7.15.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:db9c8438057e5b0f6a22a0af99c0c1d26b57fbbdbd1be5861ddb8f897fcc3a2d", size = 253178, upload-time = "2026-07-15T18:54:57.527Z" }, + { url = "https://files.pythonhosted.org/packages/f6/96/c098a6044d119c751ceede7be91035fa8310170ec24a6523aff72f0a5793/coverage-7.15.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:63022c4c8dec1d0342f05c3ede99842fe3d007689acc45e86f123a1746e4a026", size = 254934, upload-time = "2026-07-15T18:54:59.41Z" }, + { url = "https://files.pythonhosted.org/packages/b2/a2/1457b3a7a50c8d77500103b97a046db863e2f59a1cf6d2f814595f349885/coverage-7.15.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:6c0be82b4d4aa5b2704e08518e2252f3e3d110164bcca826816801052e48a7aa", size = 252898, upload-time = "2026-07-15T18:55:01.338Z" }, + { url = "https://files.pythonhosted.org/packages/6c/0e/76958874c471ecfcdde0d2b2747bb2c61bdbf34a40636f4ce9db9923e643/coverage-7.15.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4510fb9cdf6bb02dfa6af0be4a534b8102d086e22e4a33f8836df663da3d660d", size = 257056, upload-time = "2026-07-15T18:55:03.243Z" }, + { url = "https://files.pythonhosted.org/packages/7c/7c/3d7c4e3bf58baa40327dc7edc2272b17cf02299366d52763db1b0ca1556a/coverage-7.15.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:42ec3d989421b174a2ab607c1539f24127ad362757b7f1c0c0d7a2993f7eb37b", size = 252718, upload-time = "2026-07-15T18:55:05.029Z" }, + { url = "https://files.pythonhosted.org/packages/c8/b8/1cecffed9ce14fb25be9ba42d37b6bb61485c9a3ddd43cd3dde36b6087d8/coverage-7.15.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e8f91bce78e32343af184c3b7fa28fcf5a9e2641f4b6623d392038f804939188", size = 254490, upload-time = "2026-07-15T18:55:06.889Z" }, + { url = "https://files.pythonhosted.org/packages/6c/2c/42984561bc7f4c045dca67516a0c50ee5ef8d84352dbeb5559dc86c4823e/coverage-7.15.2-cp313-cp313-win32.whl", hash = "sha256:434e68d531858205895eb0d74b73d20b84260de426387d53c422a5acda2cf050", size = 223647, upload-time = "2026-07-15T18:55:08.941Z" }, + { url = "https://files.pythonhosted.org/packages/41/9f/39c7c9245efc583beddf89a87683574e663ed93637f3afb6cd7b88405676/coverage-7.15.2-cp313-cp313-win_amd64.whl", hash = "sha256:26c3b04a6377fd7c09800921fa934e3a17c0020439cd59df73e73ae1d4b6a78c", size = 224190, upload-time = "2026-07-15T18:55:10.789Z" }, + { url = "https://files.pythonhosted.org/packages/c7/de/3a2883cf8a213659280ef4b403059e17a9acaeb7fc7fd4105e1226ff2e6d/coverage-7.15.2-cp313-cp313-win_arm64.whl", hash = "sha256:3ed010aa1b69cda8e827aabfca9866216c980e2dca82ab9a78c5f83689964c8b", size = 223583, upload-time = "2026-07-15T18:55:12.678Z" }, + { url = "https://files.pythonhosted.org/packages/81/5f/aed265fd7a3551a394f36dfe41868aee709b7f95db4052205b4ad1563ac3/coverage-7.15.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:40f633c5c5fc783732f6312280122e859538fa24461235597c13d803ea9a108a", size = 221650, upload-time = "2026-07-15T18:55:14.527Z" }, + { url = "https://files.pythonhosted.org/packages/6b/2c/222ba12a545189017120f8eddfc1a0bd4616b47d5d4a8d99421edb2fe4c6/coverage-7.15.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:075560438765b7a2ef43bf7aa7758661b53d889df47f062a31bda6c1ade553a2", size = 221988, upload-time = "2026-07-15T18:55:16.674Z" }, + { url = "https://files.pythonhosted.org/packages/aa/38/304b5877ab46e6c290b4292cfcf3fe28245f0e5597cad7f6acc91fc7e0a4/coverage-7.15.2-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:25fd15dd40a0a2c51a500d664ca29053c09c3259d998407bf982b6e114696138", size = 253029, upload-time = "2026-07-15T18:55:18.856Z" }, + { url = "https://files.pythonhosted.org/packages/6c/58/821b533b8db9e44cf1d8a97bd525149ced40dde1d0093da02cb78e715244/coverage-7.15.2-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b9a6367e4aff723e8ee8190836836124284e8fcd4265e307c844010cfa074f3f", size = 255536, upload-time = "2026-07-15T18:55:21.027Z" }, + { url = "https://files.pythonhosted.org/packages/f1/f2/7aa06604c389d32ea7f0a6a988359a7eafc3cd3f8e7bc2e88cd2fdf0b877/coverage-7.15.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9854ca62c152874b2060772503535be2e8f53f70b8aaa7686b094888d872f984", size = 256881, upload-time = "2026-07-15T18:55:23.125Z" }, + { url = "https://files.pythonhosted.org/packages/a2/4f/1ef342339c7916d0096bc5888cc0f653882cc7bc8f897d5cb89143287c9b/coverage-7.15.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:913b6c56e110da40e035bbd168353bf7aaa2544a5eaccea5d98a4629aac156c7", size = 259196, upload-time = "2026-07-15T18:55:25.099Z" }, + { url = "https://files.pythonhosted.org/packages/fe/f4/7ed055d7a9c5ec13b161773a115a5ccc6b0081d568c31fad830806306cc7/coverage-7.15.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:aaccad4129d735a8a4d526f26929894c9a4e8ef7034566f210b176749d6906e3", size = 253036, upload-time = "2026-07-15T18:55:27.018Z" }, + { url = "https://files.pythonhosted.org/packages/14/79/ea82cca18c242a3a38b6c017da39726aa62dcb64aa635abf79b92009975c/coverage-7.15.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a164b50081fc7357331c4024ef4d17b78ba325f8380d05f5a69599a7e05257ee", size = 254887, upload-time = "2026-07-15T18:55:29.084Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ba/a136db3c0d9562b00e10b72540dbf3a33cd3bc5b95060c9308e247494623/coverage-7.15.2-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:bfd341ccf78128e72c094bc70cc25b3ef309c33c7c2c66ba3ed4309549e02de1", size = 252852, upload-time = "2026-07-15T18:55:31.184Z" }, + { url = "https://files.pythonhosted.org/packages/17/17/ea334246b16b7d059953fad6fdefa11e33c68efbd3fe37b1098120a1fac2/coverage-7.15.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:1473b3ba8e7ee0f076117b1a72c23f579a2b9e2bb742f48a8d86ea27ca93f91a", size = 257128, upload-time = "2026-07-15T18:55:33.163Z" }, + { url = "https://files.pythonhosted.org/packages/ed/c3/074fb66d46d607855f710876b117cbda562c5ab08363528e78820449f937/coverage-7.15.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:17c432b5f73ad52ef46fb06019f6fa7c66ce381961cf0f7dfd1d3a4bd3a98145", size = 252668, upload-time = "2026-07-15T18:55:35.063Z" }, + { url = "https://files.pythonhosted.org/packages/e1/c1/f620850ada9b36435921c9a3a8057013422b1d964eb4bf37fe138724d192/coverage-7.15.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:77f0ef5011df53a4bd1b35211ab122287f8d9b8d7aa1c4553e5c2deb24b1d446", size = 254325, upload-time = "2026-07-15T18:55:37.125Z" }, + { url = "https://files.pythonhosted.org/packages/cc/31/a729ca3689404493af82ef8e6ff70bd88bdda8da89aeef6ca9b387aeb2b4/coverage-7.15.2-cp314-cp314-win32.whl", hash = "sha256:f653e5d7248c1191ec988a85c72edeab46c3ff44f90639a4ed4874ec0be90243", size = 223844, upload-time = "2026-07-15T18:55:39.078Z" }, + { url = "https://files.pythonhosted.org/packages/c6/83/5d809dc808fb1698c671f3e372259bb9158e64b7ea526fc6ab7de64de9fe/coverage-7.15.2-cp314-cp314-win_amd64.whl", hash = "sha256:9911f31aad8906abe337c271343485cf20df5e70df5d2f57f9f136e7b55f26bc", size = 224331, upload-time = "2026-07-15T18:55:41.346Z" }, + { url = "https://files.pythonhosted.org/packages/16/4e/35e488548e952795829e129995c4174df33bf432b591d1aa42c8d9e4e7ad/coverage-7.15.2-cp314-cp314-win_arm64.whl", hash = "sha256:e38def96ad59853824c97953fdcd2c320a84ba3ce99b417db78af8bb6c3db635", size = 223760, upload-time = "2026-07-15T18:55:43.518Z" }, + { url = "https://files.pythonhosted.org/packages/ed/49/dd2c86cd6374038f6e415fb5bfb86db5218553209c081384a020369dee79/coverage-7.15.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:835ec4e20b45f0a7f63ed78f94065aca00de033403df8377bfe8b9c6abc0a7be", size = 222384, upload-time = "2026-07-15T18:55:45.569Z" }, + { url = "https://files.pythonhosted.org/packages/d3/74/173ff17a1c0808e5a438f549f6f145d5ac7528f2791310b63523e3200ac7/coverage-7.15.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7466cc7ab6dc0db871d264bf99e8779f0917ee63d40730af0552f71535a6e072", size = 222647, upload-time = "2026-07-15T18:55:47.544Z" }, + { url = "https://files.pythonhosted.org/packages/84/f8/b8cba872162356fb44ac79c10309d987206a4461e32072fc29228dad7331/coverage-7.15.2-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e370c12133095ff18432de8c044962be85a5a96d90c6fcbce8e17e76236d2328", size = 264013, upload-time = "2026-07-15T18:55:49.768Z" }, + { url = "https://files.pythonhosted.org/packages/ee/67/a807a7586d0b8cae485308ddd55756f0806c92f8e0b411bacbf23c48edf3/coverage-7.15.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fe41909c9515c3bfdb5f02c4d1f857dba322d9a9a1178069b91eea77889df63a", size = 266135, upload-time = "2026-07-15T18:55:51.941Z" }, + { url = "https://files.pythonhosted.org/packages/ce/67/cd78771dc985f7e4ebdcc82b1a96d9a932af9e806f01f2f91a89f4c72e80/coverage-7.15.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6aa28cfb6488e5453b5b762d65f73aa586380f6693a04d58078ce228a29b06c0", size = 268555, upload-time = "2026-07-15T18:55:54.065Z" }, + { url = "https://files.pythonhosted.org/packages/18/3e/10134cf81275188c58568f324fc74aedff32c63ca4d5bbc513a91944a6f0/coverage-7.15.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bcc0aae933921d03096f53b0b03eeb702129fd406dee59f08d2efacc68681fa5", size = 269674, upload-time = "2026-07-15T18:55:56.066Z" }, + { url = "https://files.pythonhosted.org/packages/75/4a/771b77de446cba985dc414bbc5844bd21604da05dbc044286df8318a48a7/coverage-7.15.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7c63387e21ab21f512c69c9756a8c7dadd322c7275edb064064433c9a09c3743", size = 263101, upload-time = "2026-07-15T18:55:58.107Z" }, + { url = "https://files.pythonhosted.org/packages/5f/b5/70a7011da15f4071943361183aefa27847f3e3aec4fd335f1cb3d3a622b1/coverage-7.15.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0e55510bc98ae943cece9e667a6c0fe94c6a92913720dea34243657a17993d0c", size = 266007, upload-time = "2026-07-15T18:56:00.468Z" }, + { url = "https://files.pythonhosted.org/packages/b4/0d/f9547e804ce7ad49646ffeffac26699510efbe6c0f751b66fdc960c4e825/coverage-7.15.2-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:2ff08701be2d1556fc78b326c80a3e8042da09352ecb3819105f8e386c8a3071", size = 263611, upload-time = "2026-07-15T18:56:02.615Z" }, + { url = "https://files.pythonhosted.org/packages/ac/59/f576a396659c0efd351f5c1544f67c3560e89c7761cabf7f65e412beeda5/coverage-7.15.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:38c9518b7103826c403a461544e3c2e77151e8676d06eaed85911a97e962584a", size = 267344, upload-time = "2026-07-15T18:56:04.622Z" }, + { url = "https://files.pythonhosted.org/packages/7c/5d/c2e4fce3579c0cb635024293f1a32bbe26df101b3e3a69f22243d1352b6c/coverage-7.15.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:dee88b1ed88587abd8c0269a1fc1f4cc77f7750d1dfde2869e2a123af420e67d", size = 262456, upload-time = "2026-07-15T18:56:06.641Z" }, + { url = "https://files.pythonhosted.org/packages/bb/dd/956287d69436b66094bc4b57ac2da71e43bfd2a5524e958900b9f582fcf8/coverage-7.15.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2fbeeeecea279727f8ac16c8e1133ddfeee793e985c86ae343d6a5ce744eef8c", size = 264771, upload-time = "2026-07-15T18:56:08.795Z" }, + { url = "https://files.pythonhosted.org/packages/2c/5a/6f979530c2734c575de77cf58f5f28d51f7123a94b5030fd9156fe5f363c/coverage-7.15.2-cp314-cp314t-win32.whl", hash = "sha256:cb0fddaa6884be6aae36ced9544b5e90f7d5f03845a2853bf47a14953a4e8688", size = 224151, upload-time = "2026-07-15T18:56:10.856Z" }, + { url = "https://files.pythonhosted.org/packages/54/7e/27f6b2a74d484742f4017553e710b01e396b23d809df3e95ca0bb9a2824b/coverage-7.15.2-cp314-cp314t-win_amd64.whl", hash = "sha256:77f091ea3a9cc611cd29f433565476bc1936c084ac8eee00ea0e7e70c27e4199", size = 224981, upload-time = "2026-07-15T18:56:12.928Z" }, + { url = "https://files.pythonhosted.org/packages/b1/48/284863423aa474240f6842bd00d680da22f4e6ea2e466618ef7c9c9e69a9/coverage-7.15.2-cp314-cp314t-win_arm64.whl", hash = "sha256:6fc448c377d6eeb00a47c673494bd9bae29280ca53987e1869e67ebedfe20658", size = 224294, upload-time = "2026-07-15T18:56:15.156Z" }, + { url = "https://files.pythonhosted.org/packages/ec/82/32e3bd191d498e64f6f911ad55d14006a0861e54869d2d32452326399e65/coverage-7.15.2-py3-none-any.whl", hash = "sha256:eb6bcae8d1a9d305351ecb108232441d11c5cfe9de840a04388ba5d2db8d735c", size = 213375, upload-time = "2026-07-15T18:56:17.305Z" }, ] [package.optional-dependencies] @@ -700,7 +711,7 @@ wheels = [ [[package]] name = "cyclopts" -version = "4.10.1" +version = "4.22.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "attrs" }, @@ -710,18 +721,18 @@ dependencies = [ { name = "tomli", marker = "python_full_version < '3.11'" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/6c/c4/2ce2ca1451487dc7d59f09334c3fa1182c46cfcf0a2d5f19f9b26d53ac74/cyclopts-4.10.1.tar.gz", hash = "sha256:ad4e4bb90576412d32276b14a76f55d43353753d16217f2c3cd5bdceba7f15a0", size = 166623, upload-time = "2026-03-23T14:43:01.098Z" } +sdist = { url = "https://files.pythonhosted.org/packages/3d/47/32d992e829f63aedea5b93360db23c8882c9bbbde094bcf0fff899ea8a3b/cyclopts-4.22.1.tar.gz", hash = "sha256:49cd3779da7113a96ac5c23b151aa61ac9ae1b4b1fe813594d207ca843c97892", size = 193551, upload-time = "2026-07-20T17:38:59.38Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8a/0b/2261922126b2e50c601fe22d7ff5194e0a4d50e654836260c0665e24d862/cyclopts-4.10.1-py3-none-any.whl", hash = "sha256:35f37257139380a386d9fe4475e1e7c87ca7795765ef4f31abba579fcfcb6ecd", size = 204331, upload-time = "2026-03-23T14:43:02.625Z" }, + { url = "https://files.pythonhosted.org/packages/9a/89/f2710638cd2f824cb976777e63ef6ed1e83fc56888cf5c9f5a053490b7be/cyclopts-4.22.1-py3-none-any.whl", hash = "sha256:9b614e231075aee9849c0bfd78f7611ab7adf417f16af5b9e42b9ed6e18c17d1", size = 232953, upload-time = "2026-07-20T17:38:58.078Z" }, ] [[package]] name = "decorator" -version = "5.2.1" +version = "5.3.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/43/fa/6d96a0978d19e17b68d634497769987b16c8f4cd0a7a05048bec693caa6b/decorator-5.2.1.tar.gz", hash = "sha256:65f266143752f734b0a7cc83c46f4618af75b8c5911b00ccb61d0ac9b6da0360", size = 56711, upload-time = "2025-02-24T04:41:34.073Z" } +sdist = { url = "https://files.pythonhosted.org/packages/60/8b/32f9823da46cde7df2087faa08cd98d01b908f8dcab982cdba9c84e85355/decorator-5.3.1.tar.gz", hash = "sha256:4cbcdd55a6efadb9dbea26b858f4fb3264567b52d69ca0d25b721b553f60ea82", size = 58084, upload-time = "2026-05-18T06:03:28.057Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl", hash = "sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a", size = 9190, upload-time = "2025-02-24T04:41:32.565Z" }, + { url = "https://files.pythonhosted.org/packages/05/7f/798705f5296a58ca505d600456748d1be48078eac8a7050d8a98bc9edb89/decorator-5.3.1-py3-none-any.whl", hash = "sha256:f47fe6fdbd2edd623ecfe36875d37aba411624e2670dd395dddae1358689bb3c", size = 10365, upload-time = "2026-05-18T06:03:26.517Z" }, ] [[package]] @@ -753,20 +764,11 @@ wheels = [ [[package]] name = "docstring-parser" -version = "0.17.0" +version = "0.18.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b2/9d/c3b43da9515bd270df0f80548d9944e389870713cc1fe2b8fb35fe2bcefd/docstring_parser-0.17.0.tar.gz", hash = "sha256:583de4a309722b3315439bb31d64ba3eebada841f2e2cee23b99df001434c912", size = 27442, upload-time = "2025-07-21T07:35:01.868Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/4d/f332313098c1de1b2d2ff91cf2674415cc7cddab2ca1b01ae29774bd5fdf/docstring_parser-0.18.0.tar.gz", hash = "sha256:292510982205c12b1248696f44959db3cdd1740237a968ea1e2e7a900eeb2015", size = 29341, upload-time = "2026-04-14T04:09:19.867Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/55/e2/2537ebcff11c1ee1ff17d8d0b6f4db75873e3b0fb32c2d4a2ee31ecb310a/docstring_parser-0.17.0-py3-none-any.whl", hash = "sha256:cf2569abd23dce8099b300f9b4fa8191e9582dda731fd533daf54c4551658708", size = 36896, upload-time = "2025-07-21T07:35:00.684Z" }, -] - -[[package]] -name = "docutils" -version = "0.22.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ae/b6/03bb70946330e88ffec97aefd3ea75ba575cb2e762061e0e62a213befee8/docutils-0.22.4.tar.gz", hash = "sha256:4db53b1fde9abecbb74d91230d32ab626d94f6badfc575d6db9194a49df29968", size = 2291750, upload-time = "2025-12-18T19:00:26.443Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/02/10/5da547df7a391dcde17f59520a231527b8571e6f46fc8efb02ccb370ab12/docutils-0.22.4-py3-none-any.whl", hash = "sha256:d0013f540772d1420576855455d050a2180186c91c15779301ac2ccb3eeb68de", size = 633196, upload-time = "2025-12-18T19:00:18.077Z" }, + { url = "https://files.pythonhosted.org/packages/a7/5f/ed01f9a3cdffbd5a008556fc7b2a08ddb1cc6ace7effa7340604b1d16699/docstring_parser-0.18.0-py3-none-any.whl", hash = "sha256:b3fcbed555c47d8479be0796ef7e19c2670d428d72e96da63f3a40122860374b", size = 22484, upload-time = "2026-04-14T04:09:18.638Z" }, ] [[package]] @@ -827,7 +829,7 @@ wheels = [ [[package]] name = "fastapi" -version = "0.135.2" +version = "0.139.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-doc" }, @@ -836,9 +838,9 @@ dependencies = [ { name = "typing-extensions" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c4/73/5903c4b13beae98618d64eb9870c3fac4f605523dd0312ca5c80dadbd5b9/fastapi-0.135.2.tar.gz", hash = "sha256:88a832095359755527b7f63bb4c6bc9edb8329a026189eed83d6c1afcf419d56", size = 395833, upload-time = "2026-03-23T14:12:41.697Z" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/95/d3f0ae10836324a2eab98a52b61210ac609f08200bf4bb0dc8132d32f78a/fastapi-0.139.2.tar.gz", hash = "sha256:333145a6891e9b5b3cfceb69baf817e8240cde4d4588ae5a10bf56ffacb6255e", size = 423428, upload-time = "2026-07-16T15:06:17.912Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8f/ea/18f6d0457f9efb2fc6fa594857f92810cadb03024975726db6546b3d6fcf/fastapi-0.135.2-py3-none-any.whl", hash = "sha256:0af0447d541867e8db2a6a25c23a8c4bd80e2394ac5529bd87501bbb9e240ca5", size = 117407, upload-time = "2026-03-23T14:12:43.284Z" }, + { url = "https://files.pythonhosted.org/packages/5f/c7/cb03251d9dfb177246a9809a76f189d21df32dbd4a845951881d11323b7f/fastapi-0.139.2-py3-none-any.whl", hash = "sha256:b9ad015a835173d59865e2f5d8296fbc2b317bf56a2ba1a5bfbdd03de2fd4b1c", size = 130234, upload-time = "2026-07-16T15:06:19.557Z" }, ] [[package]] @@ -879,8 +881,7 @@ dev = [ { name = "fastmcp-remote" }, { name = "inline-snapshot", extra = ["dirty-equals"] }, { name = "ipython", version = "8.39.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "ipython", version = "9.10.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, - { name = "ipython", version = "9.12.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "ipython", version = "9.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "loq" }, { name = "opentelemetry-exporter-otlp-proto-grpc" }, { name = "opentelemetry-sdk" }, @@ -1049,10 +1050,10 @@ requires-dist = [ { name = "jsonref", marker = "extra == 'gemini'", specifier = ">=1.1.0" }, { name = "jsonref", marker = "extra == 'server'", specifier = ">=1.1.0" }, { name = "jsonschema-path", marker = "extra == 'server'", specifier = ">=0.3.4" }, - { name = "mcp", marker = "extra == 'client'", specifier = "==2.0.0b2" }, - { name = "mcp", marker = "extra == 'mcp'", specifier = "==2.0.0b2" }, - { name = "mcp", marker = "extra == 'server'", specifier = "==2.0.0b2" }, - { name = "mcp-types", specifier = "==2.0.0b2" }, + { name = "mcp", marker = "extra == 'client'", specifier = ">=2.0.0,<3.0.0" }, + { name = "mcp", marker = "extra == 'mcp'", specifier = ">=2.0.0,<3.0.0" }, + { name = "mcp", marker = "extra == 'server'", specifier = ">=2.0.0,<3.0.0" }, + { name = "mcp-types", specifier = ">=2.0.0,<3.0.0" }, { name = "openai", marker = "extra == 'openai'", specifier = ">=1.102.0" }, { name = "openapi-pydantic", marker = "extra == 'server'", specifier = ">=0.5.1" }, { name = "opentelemetry-api", marker = "extra == 'client'", specifier = ">=1.28.0" }, @@ -1101,15 +1102,15 @@ requires-dist = [ [[package]] name = "google-auth" -version = "2.49.1" +version = "2.55.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cryptography" }, { name = "pyasn1-modules" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ea/80/6a696a07d3d3b0a92488933532f03dbefa4a24ab80fb231395b9a2a1be77/google_auth-2.49.1.tar.gz", hash = "sha256:16d40da1c3c5a0533f57d268fe72e0ebb0ae1cc3b567024122651c045d879b64", size = 333825, upload-time = "2026-03-12T19:30:58.135Z" } +sdist = { url = "https://files.pythonhosted.org/packages/79/b9/e370d86fea3da13ec0256df30323dd26c0cb9c8c85f0c6ec42ac9df0106b/google_auth-2.55.2.tar.gz", hash = "sha256:97ae7790ff740f2bc9db60eb864a7804f4ac19f5f02c38b3d942f2fea6e9b9ae", size = 361414, upload-time = "2026-07-07T18:43:21.227Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e9/eb/c6c2478d8a8d633460be40e2a8a6f8f429171997a35a96f81d3b680dec83/google_auth-2.49.1-py3-none-any.whl", hash = "sha256:195ebe3dca18eddd1b3db5edc5189b76c13e96f29e73043b923ebcf3f1a860f7", size = 240737, upload-time = "2026-03-12T19:30:53.159Z" }, + { url = "https://files.pythonhosted.org/packages/1e/c6/02eb5a337ac316a4c30c012e747bad5cea36e1a876efecdf80865541f7d8/google_auth-2.55.2-py3-none-any.whl", hash = "sha256:d715f265f2cafc6a5f1bf0dc19870d20e3119f6f6682785a250bce3d03d38a3b", size = 256778, upload-time = "2026-07-07T18:43:19.52Z" }, ] [package.optional-dependencies] @@ -1119,7 +1120,7 @@ requests = [ [[package]] name = "google-genai" -version = "1.69.0" +version = "2.12.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -1133,91 +1134,91 @@ dependencies = [ { name = "typing-extensions" }, { name = "websockets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/00/5e/c0a5e6ff60d18d3f19819a9b1fbd6a1ef2162d025696d8660550739168dc/google_genai-1.69.0.tar.gz", hash = "sha256:5f1a6a478e0c5851506a3d337534bab27b3c33120e27bf9174507ea79dfb8673", size = 519538, upload-time = "2026-03-28T15:33:27.308Z" } +sdist = { url = "https://files.pythonhosted.org/packages/96/59/9ea84cbeb8f09694564d3b0ee9dd59003551b308d47b61f251415df93982/google_genai-2.12.1.tar.gz", hash = "sha256:78c25217885d63dc430ca7c4526853512b164a25a93a8a0d0af5b85971aa1db0", size = 636710, upload-time = "2026-07-16T16:15:02.035Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/42/58/ef0586019f54b2ebb36deed7608ccb5efe1377564d2aaea6b1e295d1fadc/google_genai-1.69.0-py3-none-any.whl", hash = "sha256:252e714d724aba74949647b9de511a6a6f7804b3b317ab39ddee9cc2f001cacc", size = 760551, upload-time = "2026-03-28T15:33:24.957Z" }, + { url = "https://files.pythonhosted.org/packages/b6/b4/1369fb413fc2ba7f78acace5590b6e9990c52ab5d1d166aafaa1ae2c28c8/google_genai-2.12.1-py3-none-any.whl", hash = "sha256:686d5ec39bda345151d3ed1bac3915f01f49138b1ea519af2eb98f11cc55ebc4", size = 1023403, upload-time = "2026-07-16T16:14:59.79Z" }, ] [[package]] name = "googleapis-common-protos" -version = "1.73.1" +version = "1.75.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "protobuf" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a1/c0/4a54c386282c13449eca8bbe2ddb518181dc113e78d240458a68856b4d69/googleapis_common_protos-1.73.1.tar.gz", hash = "sha256:13114f0e9d2391756a0194c3a8131974ed7bffb06086569ba193364af59163b6", size = 147506, upload-time = "2026-03-26T22:17:38.451Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/c8/f439cffde755cffa462bfbb156278fa6f9d09119719af9814b858fd4f81f/googleapis_common_protos-1.75.0.tar.gz", hash = "sha256:53a062ff3c32552fbd62c11fe23768b78e4ddf0494d5e5fd97d3f4689c75fbbd", size = 151035, upload-time = "2026-05-07T08:04:49.423Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/dc/82/fcb6520612bec0c39b973a6c0954b6a0d948aadfe8f7e9487f60ceb8bfa6/googleapis_common_protos-1.73.1-py3-none-any.whl", hash = "sha256:e51f09eb0a43a8602f5a915870972e6b4a394088415c79d79605a46d8e826ee8", size = 297556, upload-time = "2026-03-26T22:15:58.455Z" }, + { url = "https://files.pythonhosted.org/packages/e7/c8/e2645aa8ed02fd4c7a2f59d68783b65b1f3cbdfe39a6308e156509d1fee8/googleapis_common_protos-1.75.0-py3-none-any.whl", hash = "sha256:961ed60399c457ceb0ee8f285a84c870aabc9c6a832b9d37bb281b5bebde43ed", size = 300631, upload-time = "2026-05-07T08:03:30.345Z" }, ] [[package]] name = "griffelib" -version = "2.0.2" +version = "2.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9d/82/74f4a3310cdabfbb10da554c3a672847f1ed33c6f61dd472681ce7f1fe67/griffelib-2.0.2.tar.gz", hash = "sha256:3cf20b3bc470e83763ffbf236e0076b1211bac1bc67de13daf494640f2de707e", size = 166461, upload-time = "2026-03-27T11:34:51.091Z" } +sdist = { url = "https://files.pythonhosted.org/packages/33/e4/8d187ea29c2e30b3a09505c567513077d6117861bde1fbd997a167f262ec/griffelib-2.1.0.tar.gz", hash = "sha256:762a186d2c6fd6794d4ea20d428d597ffb857cb56b66421651cbba15bdd5e813", size = 216234, upload-time = "2026-06-19T12:05:42.278Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/11/8c/c9138d881c79aa0ea9ed83cbd58d5ca75624378b38cee225dcf5c42cc91f/griffelib-2.0.2-py3-none-any.whl", hash = "sha256:925c857658fb1ba40c0772c37acbc2ab650bd794d9c1b9726922e36ea4117ea1", size = 142357, upload-time = "2026-03-27T11:34:46.275Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d3/5268aeabf2ad82658c4e2ff3a060648d0f02f3926cb53247c0e4d0dab49e/griffelib-2.1.0-py3-none-any.whl", hash = "sha256:cc7b3d2d2865ad0b909fcc38086e3f554b5ea7acbaa7bbb7ecaa3f5dfb7d9f00", size = 142560, upload-time = "2026-06-19T12:05:38.742Z" }, ] [[package]] name = "grpcio" -version = "1.78.0" +version = "1.82.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/06/8a/3d098f35c143a89520e568e6539cc098fcd294495910e359889ce8741c84/grpcio-1.78.0.tar.gz", hash = "sha256:7382b95189546f375c174f53a5fa873cef91c4b8005faa05cc5b3beea9c4f1c5", size = 12852416, upload-time = "2026-02-06T09:57:18.093Z" } +sdist = { url = "https://files.pythonhosted.org/packages/90/bc/656b89387d6f4ed7e0686c7b64c2ae7e554a759aa58122c8e5fb99392c32/grpcio-1.82.1.tar.gz", hash = "sha256:707b24abd90fcb1e45bcc080577da1dbf9971d107490589b9539af8e1e77b4b5", size = 13187300, upload-time = "2026-07-08T12:36:16.588Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5a/a8/690a085b4d1fe066130de97a87de32c45062cf2ecd218df9675add895550/grpcio-1.78.0-cp310-cp310-linux_armv7l.whl", hash = "sha256:7cc47943d524ee0096f973e1081cb8f4f17a4615f2116882a5f1416e4cfe92b5", size = 5946986, upload-time = "2026-02-06T09:54:34.043Z" }, - { url = "https://files.pythonhosted.org/packages/c7/1b/e5213c5c0ced9d2d92778d30529ad5bb2dcfb6c48c4e2d01b1f302d33d64/grpcio-1.78.0-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:c3f293fdc675ccba4db5a561048cca627b5e7bd1c8a6973ffedabe7d116e22e2", size = 11816533, upload-time = "2026-02-06T09:54:37.04Z" }, - { url = "https://files.pythonhosted.org/packages/18/37/1ba32dccf0a324cc5ace744c44331e300b000a924bf14840f948c559ede7/grpcio-1.78.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:10a9a644b5dd5aec3b82b5b0b90d41c0fa94c85ef42cb42cf78a23291ddb5e7d", size = 6519964, upload-time = "2026-02-06T09:54:40.268Z" }, - { url = "https://files.pythonhosted.org/packages/ed/f5/c0e178721b818072f2e8b6fde13faaba942406c634009caf065121ce246b/grpcio-1.78.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:4c5533d03a6cbd7f56acfc9cfb44ea64f63d29091e40e44010d34178d392d7eb", size = 7198058, upload-time = "2026-02-06T09:54:42.389Z" }, - { url = "https://files.pythonhosted.org/packages/5b/b2/40d43c91ae9cd667edc960135f9f08e58faa1576dc95af29f66ec912985f/grpcio-1.78.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ff870aebe9a93a85283837801d35cd5f8814fe2ad01e606861a7fb47c762a2b7", size = 6727212, upload-time = "2026-02-06T09:54:44.91Z" }, - { url = "https://files.pythonhosted.org/packages/ed/88/9da42eed498f0efcfcd9156e48ae63c0cde3bea398a16c99fb5198c885b6/grpcio-1.78.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:391e93548644e6b2726f1bb84ed60048d4bcc424ce5e4af0843d28ca0b754fec", size = 7300845, upload-time = "2026-02-06T09:54:47.562Z" }, - { url = "https://files.pythonhosted.org/packages/23/3f/1c66b7b1b19a8828890e37868411a6e6925df5a9030bfa87ab318f34095d/grpcio-1.78.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:df2c8f3141f7cbd112a6ebbd760290b5849cda01884554f7c67acc14e7b1758a", size = 8284605, upload-time = "2026-02-06T09:54:50.475Z" }, - { url = "https://files.pythonhosted.org/packages/94/c4/ca1bd87394f7b033e88525384b4d1e269e8424ab441ea2fba1a0c5b50986/grpcio-1.78.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:bd8cb8026e5f5b50498a3c4f196f57f9db344dad829ffae16b82e4fdbaea2813", size = 7726672, upload-time = "2026-02-06T09:54:53.11Z" }, - { url = "https://files.pythonhosted.org/packages/41/09/f16e487d4cc65ccaf670f6ebdd1a17566b965c74fc3d93999d3b2821e052/grpcio-1.78.0-cp310-cp310-win32.whl", hash = "sha256:f8dff3d9777e5d2703a962ee5c286c239bf0ba173877cc68dc02c17d042e29de", size = 4076715, upload-time = "2026-02-06T09:54:55.549Z" }, - { url = "https://files.pythonhosted.org/packages/2a/32/4ce60d94e242725fd3bcc5673c04502c82a8e87b21ea411a63992dc39f8f/grpcio-1.78.0-cp310-cp310-win_amd64.whl", hash = "sha256:94f95cf5d532d0e717eed4fc1810e8e6eded04621342ec54c89a7c2f14b581bf", size = 4799157, upload-time = "2026-02-06T09:54:59.838Z" }, - { url = "https://files.pythonhosted.org/packages/86/c7/d0b780a29b0837bf4ca9580904dfb275c1fc321ded7897d620af7047ec57/grpcio-1.78.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:2777b783f6c13b92bd7b716667452c329eefd646bfb3f2e9dabea2e05dbd34f6", size = 5951525, upload-time = "2026-02-06T09:55:01.989Z" }, - { url = "https://files.pythonhosted.org/packages/c5/b1/96920bf2ee61df85a9503cb6f733fe711c0ff321a5a697d791b075673281/grpcio-1.78.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:9dca934f24c732750389ce49d638069c3892ad065df86cb465b3fa3012b70c9e", size = 11830418, upload-time = "2026-02-06T09:55:04.462Z" }, - { url = "https://files.pythonhosted.org/packages/83/0c/7c1528f098aeb75a97de2bae18c530f56959fb7ad6c882db45d9884d6edc/grpcio-1.78.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:459ab414b35f4496138d0ecd735fed26f1318af5e52cb1efbc82a09f0d5aa911", size = 6524477, upload-time = "2026-02-06T09:55:07.111Z" }, - { url = "https://files.pythonhosted.org/packages/8d/52/e7c1f3688f949058e19a011c4e0dec973da3d0ae5e033909677f967ae1f4/grpcio-1.78.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:082653eecbdf290e6e3e2c276ab2c54b9e7c299e07f4221872380312d8cf395e", size = 7198266, upload-time = "2026-02-06T09:55:10.016Z" }, - { url = "https://files.pythonhosted.org/packages/e5/61/8ac32517c1e856677282c34f2e7812d6c328fa02b8f4067ab80e77fdc9c9/grpcio-1.78.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:85f93781028ec63f383f6bc90db785a016319c561cc11151fbb7b34e0d012303", size = 6730552, upload-time = "2026-02-06T09:55:12.207Z" }, - { url = "https://files.pythonhosted.org/packages/bd/98/b8ee0158199250220734f620b12e4a345955ac7329cfd908d0bf0fda77f0/grpcio-1.78.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f12857d24d98441af6a1d5c87442d624411db486f7ba12550b07788f74b67b04", size = 7304296, upload-time = "2026-02-06T09:55:15.044Z" }, - { url = "https://files.pythonhosted.org/packages/bd/0f/7b72762e0d8840b58032a56fdbd02b78fc645b9fa993d71abf04edbc54f4/grpcio-1.78.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5397fff416b79e4b284959642a4e95ac4b0f1ece82c9993658e0e477d40551ec", size = 8288298, upload-time = "2026-02-06T09:55:17.276Z" }, - { url = "https://files.pythonhosted.org/packages/24/ae/ae4ce56bc5bb5caa3a486d60f5f6083ac3469228faa734362487176c15c5/grpcio-1.78.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:fbe6e89c7ffb48518384068321621b2a69cab509f58e40e4399fdd378fa6d074", size = 7730953, upload-time = "2026-02-06T09:55:19.545Z" }, - { url = "https://files.pythonhosted.org/packages/b5/6e/8052e3a28eb6a820c372b2eb4b5e32d195c661e137d3eca94d534a4cfd8a/grpcio-1.78.0-cp311-cp311-win32.whl", hash = "sha256:6092beabe1966a3229f599d7088b38dfc8ffa1608b5b5cdda31e591e6500f856", size = 4076503, upload-time = "2026-02-06T09:55:21.521Z" }, - { url = "https://files.pythonhosted.org/packages/08/62/f22c98c5265dfad327251fa2f840b591b1df5f5e15d88b19c18c86965b27/grpcio-1.78.0-cp311-cp311-win_amd64.whl", hash = "sha256:1afa62af6e23f88629f2b29ec9e52ec7c65a7176c1e0a83292b93c76ca882558", size = 4799767, upload-time = "2026-02-06T09:55:24.107Z" }, - { url = "https://files.pythonhosted.org/packages/4e/f4/7384ed0178203d6074446b3c4f46c90a22ddf7ae0b3aee521627f54cfc2a/grpcio-1.78.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:f9ab915a267fc47c7e88c387a3a28325b58c898e23d4995f765728f4e3dedb97", size = 5913985, upload-time = "2026-02-06T09:55:26.832Z" }, - { url = "https://files.pythonhosted.org/packages/81/ed/be1caa25f06594463f685b3790b320f18aea49b33166f4141bfdc2bfb236/grpcio-1.78.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:3f8904a8165ab21e07e58bf3e30a73f4dffc7a1e0dbc32d51c61b5360d26f43e", size = 11811853, upload-time = "2026-02-06T09:55:29.224Z" }, - { url = "https://files.pythonhosted.org/packages/24/a7/f06d151afc4e64b7e3cc3e872d331d011c279aaab02831e40a81c691fb65/grpcio-1.78.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:859b13906ce098c0b493af92142ad051bf64c7870fa58a123911c88606714996", size = 6475766, upload-time = "2026-02-06T09:55:31.825Z" }, - { url = "https://files.pythonhosted.org/packages/8a/a8/4482922da832ec0082d0f2cc3a10976d84a7424707f25780b82814aafc0a/grpcio-1.78.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:b2342d87af32790f934a79c3112641e7b27d63c261b8b4395350dad43eff1dc7", size = 7170027, upload-time = "2026-02-06T09:55:34.7Z" }, - { url = "https://files.pythonhosted.org/packages/54/bf/f4a3b9693e35d25b24b0b39fa46d7d8a3c439e0a3036c3451764678fec20/grpcio-1.78.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:12a771591ae40bc65ba67048fa52ef4f0e6db8279e595fd349f9dfddeef571f9", size = 6690766, upload-time = "2026-02-06T09:55:36.902Z" }, - { url = "https://files.pythonhosted.org/packages/c7/b9/521875265cc99fe5ad4c5a17010018085cae2810a928bf15ebe7d8bcd9cc/grpcio-1.78.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:185dea0d5260cbb2d224c507bf2a5444d5abbb1fa3594c1ed7e4c709d5eb8383", size = 7266161, upload-time = "2026-02-06T09:55:39.824Z" }, - { url = "https://files.pythonhosted.org/packages/05/86/296a82844fd40a4ad4a95f100b55044b4f817dece732bf686aea1a284147/grpcio-1.78.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:51b13f9aed9d59ee389ad666b8c2214cc87b5de258fa712f9ab05f922e3896c6", size = 8253303, upload-time = "2026-02-06T09:55:42.353Z" }, - { url = "https://files.pythonhosted.org/packages/f3/e4/ea3c0caf5468537f27ad5aab92b681ed7cc0ef5f8c9196d3fd42c8c2286b/grpcio-1.78.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fd5f135b1bd58ab088930b3c613455796dfa0393626a6972663ccdda5b4ac6ce", size = 7698222, upload-time = "2026-02-06T09:55:44.629Z" }, - { url = "https://files.pythonhosted.org/packages/d7/47/7f05f81e4bb6b831e93271fb12fd52ba7b319b5402cbc101d588f435df00/grpcio-1.78.0-cp312-cp312-win32.whl", hash = "sha256:94309f498bcc07e5a7d16089ab984d42ad96af1d94b5a4eb966a266d9fcabf68", size = 4066123, upload-time = "2026-02-06T09:55:47.644Z" }, - { url = "https://files.pythonhosted.org/packages/ad/e7/d6914822c88aa2974dbbd10903d801a28a19ce9cd8bad7e694cbbcf61528/grpcio-1.78.0-cp312-cp312-win_amd64.whl", hash = "sha256:9566fe4ababbb2610c39190791e5b829869351d14369603702e890ef3ad2d06e", size = 4797657, upload-time = "2026-02-06T09:55:49.86Z" }, - { url = "https://files.pythonhosted.org/packages/05/a9/8f75894993895f361ed8636cd9237f4ab39ef87fd30db17467235ed1c045/grpcio-1.78.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:ce3a90455492bf8bfa38e56fbbe1dbd4f872a3d8eeaf7337dc3b1c8aa28c271b", size = 5920143, upload-time = "2026-02-06T09:55:52.035Z" }, - { url = "https://files.pythonhosted.org/packages/55/06/0b78408e938ac424100100fd081189451b472236e8a3a1f6500390dc4954/grpcio-1.78.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:2bf5e2e163b356978b23652c4818ce4759d40f4712ee9ec5a83c4be6f8c23a3a", size = 11803926, upload-time = "2026-02-06T09:55:55.494Z" }, - { url = "https://files.pythonhosted.org/packages/88/93/b59fe7832ff6ae3c78b813ea43dac60e295fa03606d14d89d2e0ec29f4f3/grpcio-1.78.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8f2ac84905d12918e4e55a16da17939eb63e433dc11b677267c35568aa63fc84", size = 6478628, upload-time = "2026-02-06T09:55:58.533Z" }, - { url = "https://files.pythonhosted.org/packages/ed/df/e67e3734527f9926b7d9c0dde6cd998d1d26850c3ed8eeec81297967ac67/grpcio-1.78.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:b58f37edab4a3881bc6c9bca52670610e0c9ca14e2ea3cf9debf185b870457fb", size = 7173574, upload-time = "2026-02-06T09:56:01.786Z" }, - { url = "https://files.pythonhosted.org/packages/a6/62/cc03fffb07bfba982a9ec097b164e8835546980aec25ecfa5f9c1a47e022/grpcio-1.78.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:735e38e176a88ce41840c21bb49098ab66177c64c82426e24e0082500cc68af5", size = 6692639, upload-time = "2026-02-06T09:56:04.529Z" }, - { url = "https://files.pythonhosted.org/packages/bf/9a/289c32e301b85bdb67d7ec68b752155e674ee3ba2173a1858f118e399ef3/grpcio-1.78.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2045397e63a7a0ee7957c25f7dbb36ddc110e0cfb418403d110c0a7a68a844e9", size = 7268838, upload-time = "2026-02-06T09:56:08.397Z" }, - { url = "https://files.pythonhosted.org/packages/0e/79/1be93f32add280461fa4773880196572563e9c8510861ac2da0ea0f892b6/grpcio-1.78.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:a9f136fbafe7ccf4ac7e8e0c28b31066e810be52d6e344ef954a3a70234e1702", size = 8251878, upload-time = "2026-02-06T09:56:10.914Z" }, - { url = "https://files.pythonhosted.org/packages/65/65/793f8e95296ab92e4164593674ae6291b204bb5f67f9d4a711489cd30ffa/grpcio-1.78.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:748b6138585379c737adc08aeffd21222abbda1a86a0dca2a39682feb9196c20", size = 7695412, upload-time = "2026-02-06T09:56:13.593Z" }, - { url = "https://files.pythonhosted.org/packages/1c/9f/1e233fe697ecc82845942c2822ed06bb522e70d6771c28d5528e4c50f6a4/grpcio-1.78.0-cp313-cp313-win32.whl", hash = "sha256:271c73e6e5676afe4fc52907686670c7cea22ab2310b76a59b678403ed40d670", size = 4064899, upload-time = "2026-02-06T09:56:15.601Z" }, - { url = "https://files.pythonhosted.org/packages/4d/27/d86b89e36de8a951501fb06a0f38df19853210f341d0b28f83f4aa0ffa08/grpcio-1.78.0-cp313-cp313-win_amd64.whl", hash = "sha256:f2d4e43ee362adfc05994ed479334d5a451ab7bc3f3fee1b796b8ca66895acb4", size = 4797393, upload-time = "2026-02-06T09:56:17.882Z" }, - { url = "https://files.pythonhosted.org/packages/29/f2/b56e43e3c968bfe822fa6ce5bca10d5c723aa40875b48791ce1029bb78c7/grpcio-1.78.0-cp314-cp314-linux_armv7l.whl", hash = "sha256:e87cbc002b6f440482b3519e36e1313eb5443e9e9e73d6a52d43bd2004fcfd8e", size = 5920591, upload-time = "2026-02-06T09:56:20.758Z" }, - { url = "https://files.pythonhosted.org/packages/5d/81/1f3b65bd30c334167bfa8b0d23300a44e2725ce39bba5b76a2460d85f745/grpcio-1.78.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:c41bc64626db62e72afec66b0c8a0da76491510015417c127bfc53b2fe6d7f7f", size = 11813685, upload-time = "2026-02-06T09:56:24.315Z" }, - { url = "https://files.pythonhosted.org/packages/0e/1c/bbe2f8216a5bd3036119c544d63c2e592bdf4a8ec6e4a1867592f4586b26/grpcio-1.78.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8dfffba826efcf366b1e3ccc37e67afe676f290e13a3b48d31a46739f80a8724", size = 6487803, upload-time = "2026-02-06T09:56:27.367Z" }, - { url = "https://files.pythonhosted.org/packages/16/5c/a6b2419723ea7ddce6308259a55e8e7593d88464ce8db9f4aa857aba96fa/grpcio-1.78.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:74be1268d1439eaaf552c698cdb11cd594f0c49295ae6bb72c34ee31abbe611b", size = 7173206, upload-time = "2026-02-06T09:56:29.876Z" }, - { url = "https://files.pythonhosted.org/packages/df/1e/b8801345629a415ea7e26c83d75eb5dbe91b07ffe5210cc517348a8d4218/grpcio-1.78.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:be63c88b32e6c0f1429f1398ca5c09bc64b0d80950c8bb7807d7d7fb36fb84c7", size = 6693826, upload-time = "2026-02-06T09:56:32.305Z" }, - { url = "https://files.pythonhosted.org/packages/34/84/0de28eac0377742679a510784f049738a80424b17287739fc47d63c2439e/grpcio-1.78.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:3c586ac70e855c721bda8f548d38c3ca66ac791dc49b66a8281a1f99db85e452", size = 7277897, upload-time = "2026-02-06T09:56:34.915Z" }, - { url = "https://files.pythonhosted.org/packages/ca/9c/ad8685cfe20559a9edb66f735afdcb2b7d3de69b13666fdfc542e1916ebd/grpcio-1.78.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:35eb275bf1751d2ffbd8f57cdbc46058e857cf3971041521b78b7db94bdaf127", size = 8252404, upload-time = "2026-02-06T09:56:37.553Z" }, - { url = "https://files.pythonhosted.org/packages/3c/05/33a7a4985586f27e1de4803887c417ec7ced145ebd069bc38a9607059e2b/grpcio-1.78.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:207db540302c884b8848036b80db352a832b99dfdf41db1eb554c2c2c7800f65", size = 7696837, upload-time = "2026-02-06T09:56:40.173Z" }, - { url = "https://files.pythonhosted.org/packages/73/77/7382241caf88729b106e49e7d18e3116216c778e6a7e833826eb96de22f7/grpcio-1.78.0-cp314-cp314-win32.whl", hash = "sha256:57bab6deef2f4f1ca76cc04565df38dc5713ae6c17de690721bdf30cb1e0545c", size = 4142439, upload-time = "2026-02-06T09:56:43.258Z" }, - { url = "https://files.pythonhosted.org/packages/48/b2/b096ccce418882fbfda4f7496f9357aaa9a5af1896a9a7f60d9f2b275a06/grpcio-1.78.0-cp314-cp314-win_amd64.whl", hash = "sha256:dce09d6116df20a96acfdbf85e4866258c3758180e8c49845d6ba8248b6d0bbb", size = 4929852, upload-time = "2026-02-06T09:56:45.885Z" }, + { url = "https://files.pythonhosted.org/packages/a3/14/5d05bfd85c101cbe44a12d7c1cea9c40698e0438cddf3a70019f735b5a27/grpcio-1.82.1-cp310-cp310-linux_armv7l.whl", hash = "sha256:91859d1cac5f47caec5fc40e9f827500cdb54ce5b36450dc9a65616b5af49c17", size = 6177087, upload-time = "2026-07-08T12:34:06.825Z" }, + { url = "https://files.pythonhosted.org/packages/19/2e/c906f8e6d0b54c0137885fff6f7b5883c6bbc381b44a0ba5ea07d7d1579b/grpcio-1.82.1-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:c80c9741dcef192f669876a81957cf7713b441c2f0c43631350d75fa49321d31", size = 11960907, upload-time = "2026-07-08T12:34:10.583Z" }, + { url = "https://files.pythonhosted.org/packages/de/be/ec4aa76cdf25539b9e960cbb9d5739f892ea6cde58078b5293860c1159d3/grpcio-1.82.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b89cff456796d2f0581783726ad017a2c70aff2d27b0f05504c34e2e417f7560", size = 6754802, upload-time = "2026-07-08T12:34:13.082Z" }, + { url = "https://files.pythonhosted.org/packages/e6/dd/47519c2a8fd9db47ec4493f44bd9f5b0175307e07089b1132e54b7b5b19c/grpcio-1.82.1-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:d6e8a08f7038ba7a77f71e250804e4aba84fe91d22cfc54ff43c07b7529c4728", size = 7484535, upload-time = "2026-07-08T12:34:15.164Z" }, + { url = "https://files.pythonhosted.org/packages/63/99/659711e9689c4dd553bcd4eacff9cb9f458f34b60edf7afb3bbc1b0a58a2/grpcio-1.82.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:50fd2fe83426b1b1c6cdc4d72d555223b7dddf8ce07c5bac218b13fc6d684c6f", size = 6919066, upload-time = "2026-07-08T12:34:17.367Z" }, + { url = "https://files.pythonhosted.org/packages/29/39/f2b772356b4f593ffe439795509fcbf675b0ff98211ae8ce2a180f2e559f/grpcio-1.82.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b758540a24d5394a9c578bf9f6126389f474b106ac3d9df1d53de56cb14c9fd9", size = 7525855, upload-time = "2026-07-08T12:34:19.479Z" }, + { url = "https://files.pythonhosted.org/packages/a7/06/b28cfffb989a84d8272593498bddd2d68148cce1813ad55189c469b0f1f8/grpcio-1.82.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:c4ba4aac238f685743575d9d700003ac16537cce26e7c774993134f530652464", size = 8565122, upload-time = "2026-07-08T12:34:21.951Z" }, + { url = "https://files.pythonhosted.org/packages/97/f9/54956cb0c701190cbc9d7e535c3f84acf0285c6b9ed198a902766e17c3cd/grpcio-1.82.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ed6fc621d6f366c88a60f0b971d5afd21d441d9aa561ee688de5b7acdb2cf901", size = 7933872, upload-time = "2026-07-08T12:34:24.539Z" }, + { url = "https://files.pythonhosted.org/packages/76/85/5f9cd1f965bbe4329556a212f178ae0c072b18b446cae05ed32fa8847c53/grpcio-1.82.1-cp310-cp310-win32.whl", hash = "sha256:bd2f45e46fff5b91c10997d0743a987517a7dde67c64c592835c2dcaac66f587", size = 4257373, upload-time = "2026-07-08T12:34:26.566Z" }, + { url = "https://files.pythonhosted.org/packages/93/b0/c4f42f7c69c53d27ed41643421b55908bcbe885b68f5a208135c72917c98/grpcio-1.82.1-cp310-cp310-win_amd64.whl", hash = "sha256:5e171d5f0d6a0af78ea7512783f170a44f80c165259d8773e3a354a7f991f2b5", size = 5006571, upload-time = "2026-07-08T12:34:28.778Z" }, + { url = "https://files.pythonhosted.org/packages/26/5b/e5092af97fa671ca279b3e373251af4bf87d5fbda7dc85f6a616899562a7/grpcio-1.82.1-cp311-cp311-linux_armv7l.whl", hash = "sha256:0ddb18a9a9e1f46692b3567ae4abb3f8d117ce6afea48650f8eca06d8ab5d06f", size = 6181472, upload-time = "2026-07-08T12:34:31.009Z" }, + { url = "https://files.pythonhosted.org/packages/c1/8f/18053a3a2ca03d0c2a1b8cc7271e705007a16aa5dae84bac00935c5b1a7f/grpcio-1.82.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:cf855b1af246720f567b0ce5d0724d45dfa4188eecc3296a2a69257b11b9e94b", size = 11970995, upload-time = "2026-07-08T12:34:33.603Z" }, + { url = "https://files.pythonhosted.org/packages/5b/7e/21b1acb052876ad00959ec4d1b05fe08607d650bcfa282073bb164c2703c/grpcio-1.82.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ddb30cb13e25bc13cea70ffc69d6d90c49d36ea6c1d4549e6912f70177834cac", size = 6760127, upload-time = "2026-07-08T12:34:36.122Z" }, + { url = "https://files.pythonhosted.org/packages/3e/12/25eef9c245c54f0061317d13a302357fe8ea03bac240b2b02ececcf54da4/grpcio-1.82.1-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1e822b2774f719c017cbe700b6e47173b6ae290fb84906f52a5a3c2c60b62e1e", size = 7484377, upload-time = "2026-07-08T12:34:38.368Z" }, + { url = "https://files.pythonhosted.org/packages/a0/41/1a348767eb9d9bd7765dc4fa8a01723d3bb386d67f981ee5c6f9c02b8b1c/grpcio-1.82.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5dafb1ece8ed45dee7c738f166ec82e19673221ed5ab8967f72858a4685345b2", size = 6924269, upload-time = "2026-07-08T12:34:40.583Z" }, + { url = "https://files.pythonhosted.org/packages/e4/b9/3aae7a03d34c86ea27988db859a6087c186f6c3f53f9b551e07afd989bfa/grpcio-1.82.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e06503106e7271e0a49fd5a1ac04747f1e47e87d900476db6fe45bc87ee411f4", size = 7531848, upload-time = "2026-07-08T12:34:43.277Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/3c4afa625d0dac9090707966916284c035fc5b2fb3e2c51e156accee6735/grpcio-1.82.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ff99bc8cafb6a952201c37b995f425e641c93ffa6e072258525feab57290141d", size = 8568217, upload-time = "2026-07-08T12:34:45.502Z" }, + { url = "https://files.pythonhosted.org/packages/20/9c/d8489c628e73e20a3d034e7f66912de7b1acb405f01d388f056a88e47924/grpcio-1.82.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:644ae1b94266ac785330f4590a69e52b6a7eb73029043a02209db81c81397d69", size = 7938771, upload-time = "2026-07-08T12:34:48.323Z" }, + { url = "https://files.pythonhosted.org/packages/4b/b7/0a92cfd1658f3a896d4aa12d4efeb7dd4ddfc723725ae22741a5241ea710/grpcio-1.82.1-cp311-cp311-win32.whl", hash = "sha256:e203d2e19d471630084a16c815616f8211dff21c268ab3c5f5bf38417832e074", size = 4256432, upload-time = "2026-07-08T12:34:50.432Z" }, + { url = "https://files.pythonhosted.org/packages/c7/6a/2872c761b025d9ec74386f22a4a7d59c5a5b00ebf718761b33739ffc45de/grpcio-1.82.1-cp311-cp311-win_amd64.whl", hash = "sha256:0d8299c285fe6cc6a1f56badf8d3bc5078c8d20273ee64bafa3783b4bc29a769", size = 5009633, upload-time = "2026-07-08T12:34:52.67Z" }, + { url = "https://files.pythonhosted.org/packages/dc/88/d1350bf3343a2ed87d801584e40609f6c6bd3087926eeca03de50348cf4a/grpcio-1.82.1-cp312-cp312-linux_armv7l.whl", hash = "sha256:c09bd5fa0d5b1fbd773ec349fe61441c3e4ebf168c229aa7538a820bdfad6a58", size = 6144689, upload-time = "2026-07-08T12:34:55.567Z" }, + { url = "https://files.pythonhosted.org/packages/e6/33/71875cdecd27c24ac1385d4783a09853f01b84a825a36aec2a2bc7d0d080/grpcio-1.82.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:1eae24810720734598e3e6a1a528d5de0f265fe3fc86575e9ecce424b9ec7379", size = 11952034, upload-time = "2026-07-08T12:34:58.128Z" }, + { url = "https://files.pythonhosted.org/packages/82/b2/d9125df3d8a140dec12cc82c05b7deafedeababcff6496f28b2fd5634d10/grpcio-1.82.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a6bd5daf5bde7b24d7ad2cbaf8bf9eac620d96222016bb5e7ddde930dec0673f", size = 6710772, upload-time = "2026-07-08T12:35:01.33Z" }, + { url = "https://files.pythonhosted.org/packages/88/9b/69e2d1627398b964f34437dc476a5aff5a2cc8e7f247d26272b5674b5faf/grpcio-1.82.1-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1ecfde669cb687ac020d31ff76debe5dc7a62213335f02262eb6625628da1c03", size = 7450677, upload-time = "2026-07-08T12:35:03.926Z" }, + { url = "https://files.pythonhosted.org/packages/e4/e7/8f855ca29c294956122a2a73023655b9b02602d5111dad2b9b00e7631c68/grpcio-1.82.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:011c8badee95734dee8bf05ce3464756a0ac3ebb8d443afd20c0e2b5e4640ad9", size = 6886855, upload-time = "2026-07-08T12:35:06.174Z" }, + { url = "https://files.pythonhosted.org/packages/5f/f0/fa87e85f49925f44c479d07e58b051e69bcfef6b6d5fbc6749d140f6730a/grpcio-1.82.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b85f4564926fb23114d239392bdcae200db1e6179629edd7d7ab0ab89c96a197", size = 7501323, upload-time = "2026-07-08T12:35:08.49Z" }, + { url = "https://files.pythonhosted.org/packages/67/55/2e0b10ae1d3ef9dcc480b91dc2158f4931fc4675d3af0a2836e39b2a744f/grpcio-1.82.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:2c0c8270833395644c3fe6b6a806397955a2bc0538000a19a78b90c05a6c16e0", size = 8536899, upload-time = "2026-07-08T12:35:10.975Z" }, + { url = "https://files.pythonhosted.org/packages/bd/95/a3d8b0431fa221efc51ee39d73595ede74ba82a43b7c4313192e580face2/grpcio-1.82.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2ba199205ff46c7778290fe1673c91ac8e7e45678dd5c86e9e56fa33ec8788f6", size = 7913892, upload-time = "2026-07-08T12:35:13.944Z" }, + { url = "https://files.pythonhosted.org/packages/b8/92/f2651ec704d9852a56faef394775038afba435b50ce82ab2404d119c3355/grpcio-1.82.1-cp312-cp312-win32.whl", hash = "sha256:06127691866e295c14e84a1fb86356dd962254f6abd0da4ca4b001eea9e89438", size = 4240985, upload-time = "2026-07-08T12:35:16.048Z" }, + { url = "https://files.pythonhosted.org/packages/96/4f/a5fe8bf0d0a1b24855f370293075c931f27de4eb55f0f158786095bf3c11/grpcio-1.82.1-cp312-cp312-win_amd64.whl", hash = "sha256:1fa3223a3a2e1db74f4c2b255189eb7ea875dfba56e221d252ee3fc7b204778e", size = 5001580, upload-time = "2026-07-08T12:35:18.689Z" }, + { url = "https://files.pythonhosted.org/packages/1b/3e/496992d08c0aaa11272eb6228dc8ab947da01fe835de243cd00521bce4c4/grpcio-1.82.1-cp313-cp313-linux_armv7l.whl", hash = "sha256:b454a2d97bfab7565683a02345f86bd182ab69fd7c2bdb7414171e7538f266b1", size = 6146068, upload-time = "2026-07-08T12:35:21.365Z" }, + { url = "https://files.pythonhosted.org/packages/e7/8f/f263d6f14fdba6b56cfadd91fd3e158a52682b72c6016d1f8723d435659f/grpcio-1.82.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:3dde70abfc80b3be11de53ba0d601c439e7fb2afd3583ad1788d1146bec92fdc", size = 11948600, upload-time = "2026-07-08T12:35:24.312Z" }, + { url = "https://files.pythonhosted.org/packages/8c/14/3a02e6ee49c2d85bc15eaae321e0e11ab3542cad3c5b2de121ecce0c4296/grpcio-1.82.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f5523099c98c292ea1ae08e617249db760c56a78f8deae879027fe7d1ffbcbf6", size = 6714591, upload-time = "2026-07-08T12:35:27.027Z" }, + { url = "https://files.pythonhosted.org/packages/69/80/58e3738696f48ab7645347b98d8a7f93d10e00e6218388fbfcd6c9310e3d/grpcio-1.82.1-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:5e5c4dc0a59b0f8490a6bdfd6fc8395b9d8ad8a8407c7d67ca7b5bba15c0877f", size = 7454995, upload-time = "2026-07-08T12:35:29.599Z" }, + { url = "https://files.pythonhosted.org/packages/f3/6c/2557c1a889363072fbf2285ecd0e8c44860d4dbd60f017a32537c5b863e2/grpcio-1.82.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c40d94ba820329cc191981bc22fa6f6eed0799c6d921f3c6709521d59d4a2fd7", size = 6888621, upload-time = "2026-07-08T12:35:32.38Z" }, + { url = "https://files.pythonhosted.org/packages/d2/66/907706ccaff1223f1e10fd5b37fc16faead43392fccb4e786e7e390ac141/grpcio-1.82.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4c816180e31e273caaec6f8bd86a8392499d5bbb26f41da44e3dce48bde69095", size = 7505069, upload-time = "2026-07-08T12:35:35.072Z" }, + { url = "https://files.pythonhosted.org/packages/b3/7c/ff97b0d0f635987ee5ec80dfedafa1aad629303745d48e8637d10eec5b80/grpcio-1.82.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e31fd780b261830720cb70b0fd8f0aa51d49e75a66d7464ad2e31d4b765f2580", size = 8535384, upload-time = "2026-07-08T12:35:37.954Z" }, + { url = "https://files.pythonhosted.org/packages/62/9e/a97fddd970a8d1588cade06eca20443761c1858b0ad6590a5c835aa18062/grpcio-1.82.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9d76152d7c31d7210d4a106e5d8b64da5bba5d6abf11be30e2f7b0a0c59bbcbf", size = 7910707, upload-time = "2026-07-08T12:35:40.797Z" }, + { url = "https://files.pythonhosted.org/packages/20/e4/eaba1517888af483a88d449eb7566f0f7f63446d46f339c5891798435875/grpcio-1.82.1-cp313-cp313-win32.whl", hash = "sha256:38e9dcb5258226fb3282630b31b16a968df52c8c6ad514af540646e0a4578f8a", size = 4240363, upload-time = "2026-07-08T12:35:43.298Z" }, + { url = "https://files.pythonhosted.org/packages/b0/42/66a98d47732e35290bef722f6149fed3709cd4cf61166f6f53a12f417302/grpcio-1.82.1-cp313-cp313-win_amd64.whl", hash = "sha256:3dbfb52c36d9511ac2b8e6c94fdde837b393ae520cc321f52a333a2deedf5a90", size = 5000980, upload-time = "2026-07-08T12:35:46.262Z" }, + { url = "https://files.pythonhosted.org/packages/b4/cb/cf9ae9e164c6e6dc8a494faa9771763df9da150eefe19671009624d1559f/grpcio-1.82.1-cp314-cp314-linux_armv7l.whl", hash = "sha256:35f990f7784c8fd2872644f07f96ebb4d9e48e145a190ab80d0280af91a1bfb2", size = 6146901, upload-time = "2026-07-08T12:35:49.261Z" }, + { url = "https://files.pythonhosted.org/packages/3b/2a/eccf26dbcfb7f7cab8027c5490a16c8937c5aa7a2ec20a3eab2cf7a43165/grpcio-1.82.1-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:46536a4a1f4434df3c851b9254ff6fc7df5705b273681a15ca277d5921c178a0", size = 11954756, upload-time = "2026-07-08T12:35:52.196Z" }, + { url = "https://files.pythonhosted.org/packages/ad/75/3b3b4a3cc9f084b026af96e1d3e539b1af29ec7f41ed0dfff3cb99cc8626/grpcio-1.82.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d6650a7c1ebb7921c70e12a385439a8118efb99e669fa9ed31cf25db1843937c", size = 6723087, upload-time = "2026-07-08T12:35:54.973Z" }, + { url = "https://files.pythonhosted.org/packages/9c/8b/b0f0c9b1400a99a4da4c09b114f101b192f8f11192e76f620b8962f5d90b/grpcio-1.82.1-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:b8e110c66df5204c0506d6c8787b35d48b8b699ef5aa366d6c4d67325c67fe9a", size = 7454542, upload-time = "2026-07-08T12:35:57.586Z" }, + { url = "https://files.pythonhosted.org/packages/b7/bd/428e38868382aa193697a5aa53973f29c58e58ba4268aa0c86a2715ee58b/grpcio-1.82.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f853eae07235a51a27bb5d6a9a175a59ca55dc9b99edc6ce2f76f07332d333ae", size = 6889588, upload-time = "2026-07-08T12:36:00.012Z" }, + { url = "https://files.pythonhosted.org/packages/49/ce/03e01d5e10259bf5c08ee50570cc94724e79c956f61fd2f09b341af0956c/grpcio-1.82.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:60b0f2c95337694fc094b77d9f60f50566c84b5677393e342eb98daeee242d98", size = 7514166, upload-time = "2026-07-08T12:36:02.693Z" }, + { url = "https://files.pythonhosted.org/packages/ff/59/278b4b600329e2ba3849f3c1ea3c820b3a01b38a7ad184ba09595e8d2733/grpcio-1.82.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:b064fc444812bdaa9825d33c26f8d732d63ee6a5d78557c1faf92c98687fed27", size = 8536166, upload-time = "2026-07-08T12:36:05.349Z" }, + { url = "https://files.pythonhosted.org/packages/44/27/7ccf2ef00f27a8e47a79d641c8ceaf7d3028c7a03d9a97b4c8a9a783c086/grpcio-1.82.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7d7ede11d747b4e1bd05e3bc0260e155b65a88735a895a10f6521f19b889511e", size = 7912572, upload-time = "2026-07-08T12:36:08.393Z" }, + { url = "https://files.pythonhosted.org/packages/0d/be/33742482d2753f2d3a1b7641664b6622262d44f2f3b609f13425dd86d36f/grpcio-1.82.1-cp314-cp314-win32.whl", hash = "sha256:3d21f19838dc255ecbb79321b15ae9b98fbddff4c3d4aedb0a81bdd7f4ab572a", size = 4321856, upload-time = "2026-07-08T12:36:10.899Z" }, + { url = "https://files.pythonhosted.org/packages/cc/67/03329c847172c78ddeb1eb9be6b444fdbc12775a84c958b27e427e7b926d/grpcio-1.82.1-cp314-cp314-win_amd64.whl", hash = "sha256:e20f1edbb15f99e3128ec86433f9785fd5a451d8f115e74fe0056134f092a9d5", size = 5141114, upload-time = "2026-07-08T12:36:13.595Z" }, ] [[package]] @@ -1244,15 +1245,15 @@ wheels = [ [[package]] name = "httpcore2" -version = "2.6.0" +version = "2.7.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "h11" }, { name = "truststore" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/53/db/2ad49878b36af4cff7527c1158b083ad6d9350462f1a35685cc3ebfa7c2b/httpcore2-2.6.0.tar.gz", hash = "sha256:95b692b582402ec49b3d84c2343556e4ac4c0962c8b3d39c48d485b9ecc240ab", size = 65592, upload-time = "2026-07-14T10:48:33.816Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d5/fe/6a3f9f1a8bb8733326140737446aaf72fddb8b54b8f202302f5c84960613/httpcore2-2.7.0.tar.gz", hash = "sha256:6dc0fedf329a52a990930a5579edfebaea81118ea700ea0dd7de2b5e5be49efc", size = 65593, upload-time = "2026-07-14T20:40:01.111Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c2/fa/08f483851a70ef10806e3b84240f2a8f923658035b794f7609795895d9ea/httpcore2-2.6.0-py3-none-any.whl", hash = "sha256:c237a45c7eef885cf032cb9b850d59fcf1fa7e00230307f08aab26486a6ed584", size = 81507, upload-time = "2026-07-14T10:48:31.504Z" }, + { url = "https://files.pythonhosted.org/packages/6f/6c/62e2e279e63fc4f7a5ee841ef13175a8bbc613f258e9dcc186e9de803a42/httpcore2-2.7.0-py3-none-any.whl", hash = "sha256:1452f589fe23f55b44546cd884294c41a29330af902bc0b71a761fd52d18f92b", size = 81506, upload-time = "2026-07-14T20:39:58.053Z" }, ] [[package]] @@ -1272,7 +1273,7 @@ wheels = [ [[package]] name = "httpx2" -version = "2.6.0" +version = "2.7.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -1281,9 +1282,9 @@ dependencies = [ { name = "truststore" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/89/17/1e142bf3c76684232a092e1e4002be07fd3403b1c2dcb15d0012ea300c8f/httpx2-2.6.0.tar.gz", hash = "sha256:5d362fd59562cf2139a60c67bb016587a70b36156a517f176c7cbf1587d1ab22", size = 92736, upload-time = "2026-07-14T10:48:35.12Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/4a/129b2e21b90ac2985d3928d96792bccc39bc6dfe796c5eee2d8ec06d4105/httpx2-2.7.0.tar.gz", hash = "sha256:8b30709aed5c8465b0dd3b95c09ce301c8f79e7e7a2d00ab0af551e0d0375b07", size = 94487, upload-time = "2026-07-14T20:40:02.318Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/47/86/7d82f7c6aac32433eaf0c914b8bc870ae25759413b9d7d52ea6aa15f2546/httpx2-2.6.0-py3-none-any.whl", hash = "sha256:6cccc3665d6bceb3c1c4f1422ae7e53fda67a853f0135f09b25ce0d4dcac01e3", size = 88541, upload-time = "2026-07-14T10:48:32.681Z" }, + { url = "https://files.pythonhosted.org/packages/1d/b8/c341bba6411bdfda786020343c47a75ef472f6085caf82391b142b1a3ad9/httpx2-2.7.0-py3-none-any.whl", hash = "sha256:ed2a2719c696789e09493bd8e2bec3d8bd925cc6e26b68389ec25ade132f7bf4", size = 90234, upload-time = "2026-07-14T20:39:59.531Z" }, ] [[package]] @@ -1297,14 +1298,14 @@ wheels = [ [[package]] name = "importlib-metadata" -version = "8.7.1" +version = "9.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "zipp" }, + { name = "zipp", marker = "python_full_version < '3.13'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f3/49/3b30cad09e7771a4982d9975a8cbf64f00d4a1ececb53297f1d9a7be1b10/importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb", size = 57107, upload-time = "2025-12-21T10:00:19.278Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a9/01/15bb152d77b21318514a96f43af312635eb2500c96b55398d020c93d86ea/importlib_metadata-9.0.0.tar.gz", hash = "sha256:a4f57ab599e6a2e3016d7595cfd72eb4661a5106e787a95bcc90c7105b831efc", size = 56405, upload-time = "2026-03-20T06:42:56.999Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151", size = 27865, upload-time = "2025-12-21T10:00:18.329Z" }, + { url = "https://files.pythonhosted.org/packages/38/3d/2d244233ac4f76e38533cfcb2991c9eb4c7bf688ae0a036d30725b8faafe/importlib_metadata-9.0.0-py3-none-any.whl", hash = "sha256:2d21d1cc5a017bd0559e36150c21c830ab1dc304dedd1b7ea85d20f45ef3edd7", size = 27789, upload-time = "2026-03-20T06:42:55.665Z" }, ] [[package]] @@ -1318,7 +1319,7 @@ wheels = [ [[package]] name = "inline-snapshot" -version = "0.32.5" +version = "0.35.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "asttokens" }, @@ -1328,9 +1329,9 @@ dependencies = [ { name = "tomli", marker = "python_full_version < '3.11'" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ca/87/62b78b49042c533038ab1bf0931a7b70fdb78d07a11c9bf159be04027df8/inline_snapshot-0.32.5.tar.gz", hash = "sha256:5025074eab5c82a88504975e2655beeb5e96fd57ed2d9ebb38538473748f2065", size = 2626796, upload-time = "2026-03-13T18:35:54.891Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c6/b3/38a1b9c6323c5dff0ab6c74aa839f1c7d8b7057a861d80ec5bfe834be24e/inline_snapshot-0.35.2.tar.gz", hash = "sha256:6cfd2ea0b52d9cb9beb13c1e73d740ff86f630ba48d9ea6947e54e3ff8494876", size = 2534713, upload-time = "2026-07-16T10:51:58.826Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/09/d3/73426dd3da75095fd071ce5c1f8e520e879a582ca04df861575c6feb9166/inline_snapshot-0.32.5-py3-none-any.whl", hash = "sha256:ac617c273e811ed5ca15abd8f8dbd3fa268296bb0642ccb1403a5df61ce2e39e", size = 84993, upload-time = "2026-03-13T18:35:52.955Z" }, + { url = "https://files.pythonhosted.org/packages/b7/ea/de85fc264e4c76bda3552eee42904e8b7079fd826902aa5fa044bc5bbc3c/inline_snapshot-0.35.2-py3-none-any.whl", hash = "sha256:91b230b310c95a8c3b91cdbc637e064c7c2f98c13935b64875a7ba9ef174e161", size = 94669, upload-time = "2026-07-16T10:51:57.457Z" }, ] [package.optional-dependencies] @@ -1365,53 +1366,30 @@ wheels = [ [[package]] name = "ipython" -version = "9.10.1" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version == '3.11.*'", -] -dependencies = [ - { name = "colorama", marker = "python_full_version == '3.11.*' and sys_platform == 'win32'" }, - { name = "decorator", marker = "python_full_version == '3.11.*'" }, - { name = "ipython-pygments-lexers", marker = "python_full_version == '3.11.*'" }, - { name = "jedi", marker = "python_full_version == '3.11.*'" }, - { name = "matplotlib-inline", marker = "python_full_version == '3.11.*'" }, - { name = "pexpect", marker = "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'" }, - { name = "prompt-toolkit", marker = "python_full_version == '3.11.*'" }, - { name = "pygments", marker = "python_full_version == '3.11.*'" }, - { name = "stack-data", marker = "python_full_version == '3.11.*'" }, - { name = "traitlets", marker = "python_full_version == '3.11.*'" }, - { name = "typing-extensions", marker = "python_full_version == '3.11.*'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c5/25/daae0e764047b0a2480c7bbb25d48f4f509b5818636562eeac145d06dfee/ipython-9.10.1.tar.gz", hash = "sha256:e170e9b2a44312484415bdb750492699bf329233b03f2557a9692cce6466ada4", size = 4426663, upload-time = "2026-03-27T09:53:26.244Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/01/09/ba70f8d662d5671687da55ad2cc0064cf795b15e1eea70907532202e7c97/ipython-9.10.1-py3-none-any.whl", hash = "sha256:82d18ae9fb9164ded080c71ef92a182ee35ee7db2395f67616034bebb020a232", size = 622827, upload-time = "2026-03-27T09:53:24.566Z" }, -] - -[[package]] -name = "ipython" -version = "9.12.0" +version = "9.15.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.14'", "python_full_version == '3.13.*'", - "python_full_version == '3.12.*'", + "python_full_version >= '3.11' and python_full_version < '3.13'", ] dependencies = [ - { name = "colorama", marker = "python_full_version >= '3.12' and sys_platform == 'win32'" }, - { name = "decorator", marker = "python_full_version >= '3.12'" }, - { name = "ipython-pygments-lexers", marker = "python_full_version >= '3.12'" }, - { name = "jedi", marker = "python_full_version >= '3.12'" }, - { name = "matplotlib-inline", marker = "python_full_version >= '3.12'" }, - { name = "pexpect", marker = "python_full_version >= '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'" }, - { name = "prompt-toolkit", marker = "python_full_version >= '3.12'" }, - { name = "pygments", marker = "python_full_version >= '3.12'" }, - { name = "stack-data", marker = "python_full_version >= '3.12'" }, - { name = "traitlets", marker = "python_full_version >= '3.12'" }, + { name = "colorama", marker = "python_full_version >= '3.11' and sys_platform == 'win32'" }, + { name = "decorator", marker = "python_full_version >= '3.11'" }, + { name = "ipython-pygments-lexers", marker = "python_full_version >= '3.11'" }, + { name = "jedi", marker = "python_full_version >= '3.11'" }, + { name = "matplotlib-inline", marker = "python_full_version >= '3.11'" }, + { name = "pexpect", marker = "python_full_version >= '3.11' and sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "prompt-toolkit", marker = "python_full_version >= '3.11'" }, + { name = "psutil", marker = "python_full_version >= '3.11' and sys_platform != 'cygwin' and sys_platform != 'emscripten'" }, + { name = "pygments", marker = "python_full_version >= '3.11'" }, + { name = "stack-data", marker = "python_full_version >= '3.11'" }, + { name = "traitlets", marker = "python_full_version >= '3.11'" }, + { name = "typing-extensions", marker = "python_full_version == '3.11.*'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3a/73/7114f80a8f9cabdb13c27732dce24af945b2923dcab80723602f7c8bc2d8/ipython-9.12.0.tar.gz", hash = "sha256:01daa83f504b693ba523b5a407246cabde4eb4513285a3c6acaff11a66735ee4", size = 4428879, upload-time = "2026-03-27T09:42:45.312Z" } +sdist = { url = "https://files.pythonhosted.org/packages/53/59/165d3b4d75cc34add3122c4417ecb229085140ac573103c223cd01dde96f/ipython-9.15.0.tar.gz", hash = "sha256:da2819ce2aa83135257df830660b1176d986c3d2876db24df01974fa955b2756", size = 4442580, upload-time = "2026-06-26T11:03:35.913Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/59/22/906c8108974c673ebef6356c506cebb6870d48cedea3c41e949e2dd556bb/ipython-9.12.0-py3-none-any.whl", hash = "sha256:0f2701e8ee86e117e37f50563205d36feaa259d2e08d4a6bc6b6d74b18ce128d", size = 625661, upload-time = "2026-03-27T09:42:42.831Z" }, + { url = "https://files.pythonhosted.org/packages/40/3a/948263ca3b9d65bb2b1b0c521b3a49fad5d59ada58724bd87d2bd5ff3f36/ipython-9.15.0-py3-none-any.whl", hash = "sha256:515ad9c3cdf0c932a5a9f6245419e8aba706b7bd03c3e1d3a1c83d9351d6aa6e", size = 630895, upload-time = "2026-06-26T11:03:33.809Z" }, ] [[package]] @@ -1452,26 +1430,26 @@ wheels = [ [[package]] name = "jaraco-functools" -version = "4.4.0" +version = "4.6.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "more-itertools" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/0f/27/056e0638a86749374d6f57d0b0db39f29509cce9313cf91bdc0ac4d91084/jaraco_functools-4.4.0.tar.gz", hash = "sha256:da21933b0417b89515562656547a77b4931f98176eb173644c0d35032a33d6bb", size = 19943, upload-time = "2025-12-21T09:29:43.6Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6c/1f/c23395957d41ccf27c4e535c3d334c4051e5395b3752057ba4cbaec35c56/jaraco_functools-4.6.0.tar.gz", hash = "sha256:880c577ec9720b3a052d5bc611fb9f2269b3d87902ef42440df443b88e443280", size = 20837, upload-time = "2026-07-14T01:28:02.544Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fd/c4/813bb09f0985cb21e959f21f2464169eca882656849adf727ac7bb7e1767/jaraco_functools-4.4.0-py3-none-any.whl", hash = "sha256:9eec1e36f45c818d9bf307c8948eb03b2b56cd44087b3cdc989abca1f20b9176", size = 10481, upload-time = "2025-12-21T09:29:42.27Z" }, + { url = "https://files.pythonhosted.org/packages/02/36/ecc85bc96c273dc8a11273ed4782272975e6338d4a3e9228621175edf0e3/jaraco_functools-4.6.0-py3-none-any.whl", hash = "sha256:99e3dc0060c5cbe8fcd1cdb36258e2a65ca40f1566b2033b12abb1bb44dd3c30", size = 11677, upload-time = "2026-07-14T01:28:01.59Z" }, ] [[package]] name = "jedi" -version = "0.19.2" +version = "0.20.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "parso" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/72/3a/79a912fbd4d8dd6fbb02bf69afd3bb72cf0c729bb3063c6f4498603db17a/jedi-0.19.2.tar.gz", hash = "sha256:4770dc3de41bde3966b02eb84fbcf557fb33cce26ad23da12c742fb50ecb11f0", size = 1231287, upload-time = "2024-11-11T01:41:42.873Z" } +sdist = { url = "https://files.pythonhosted.org/packages/46/b7/a3635f6a2d7cf5b5dd98064fc1d5fbbafcb25477bcea204a3a92145d158b/jedi-0.20.0.tar.gz", hash = "sha256:c3f4ccbd276696f4b19c54618d4fb18f9fc24b0aef02acf704b23f487daa1011", size = 3119416, upload-time = "2026-05-01T23:38:47.814Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c0/5a/9cac0c82afec3d09ccd97c8b6502d48f165f9124db81b4bcb90b4af974ee/jedi-0.19.2-py2.py3-none-any.whl", hash = "sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9", size = 1572278, upload-time = "2024-11-11T01:41:40.175Z" }, + { url = "https://files.pythonhosted.org/packages/9a/93/242e2eab5fe682ffcb8b0084bde703a41d51e17ee0f3a31ff0d9d813620a/jedi-0.20.0-py2.py3-none-any.whl", hash = "sha256:7bdd9c2634f56713299976f4cbd59cb3fa92165cc5e05ea811fb253480728b67", size = 4884812, upload-time = "2026-05-01T23:38:43.919Z" }, ] [[package]] @@ -1485,111 +1463,113 @@ wheels = [ [[package]] name = "jiter" -version = "0.13.0" +version = "0.16.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0d/5e/4ec91646aee381d01cdb9974e30882c9cd3b8c5d1079d6b5ff4af522439a/jiter-0.13.0.tar.gz", hash = "sha256:f2839f9c2c7e2dffc1bc5929a510e14ce0a946be9365fd1219e7ef342dae14f4", size = 164847, upload-time = "2026-02-02T12:37:56.441Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/1f/10936e16d8860c70698a1aa939a46aa0224813b782bce4e000e637da0b2d/jiter-0.16.0.tar.gz", hash = "sha256:7b24c3492c5f4f84a37946ad9cf504910cf6a782d6a4e0689b6673c5894b4a1c", size = 176431, upload-time = "2026-06-29T13:05:13.657Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d0/5a/41da76c5ea07bec1b0472b6b2fdb1b651074d504b19374d7e130e0cdfb25/jiter-0.13.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2ffc63785fd6c7977defe49b9824ae6ce2b2e2b77ce539bdaf006c26da06342e", size = 311164, upload-time = "2026-02-02T12:35:17.688Z" }, - { url = "https://files.pythonhosted.org/packages/40/cb/4a1bf994a3e869f0d39d10e11efb471b76d0ad70ecbfb591427a46c880c2/jiter-0.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4a638816427006c1e3f0013eb66d391d7a3acda99a7b0cf091eff4497ccea33a", size = 320296, upload-time = "2026-02-02T12:35:19.828Z" }, - { url = "https://files.pythonhosted.org/packages/09/82/acd71ca9b50ecebadc3979c541cd717cce2fe2bc86236f4fa597565d8f1a/jiter-0.13.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19928b5d1ce0ff8c1ee1b9bdef3b5bfc19e8304f1b904e436caf30bc15dc6cf5", size = 352742, upload-time = "2026-02-02T12:35:21.258Z" }, - { url = "https://files.pythonhosted.org/packages/71/03/d1fc996f3aecfd42eb70922edecfb6dd26421c874503e241153ad41df94f/jiter-0.13.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:309549b778b949d731a2f0e1594a3f805716be704a73bf3ad9a807eed5eb5721", size = 363145, upload-time = "2026-02-02T12:35:24.653Z" }, - { url = "https://files.pythonhosted.org/packages/f1/61/a30492366378cc7a93088858f8991acd7d959759fe6138c12a4644e58e81/jiter-0.13.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bcdabaea26cb04e25df3103ce47f97466627999260290349a88c8136ecae0060", size = 487683, upload-time = "2026-02-02T12:35:26.162Z" }, - { url = "https://files.pythonhosted.org/packages/20/4e/4223cffa9dbbbc96ed821c5aeb6bca510848c72c02086d1ed3f1da3d58a7/jiter-0.13.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a3a377af27b236abbf665a69b2bdd680e3b5a0bd2af825cd3b81245279a7606c", size = 373579, upload-time = "2026-02-02T12:35:27.582Z" }, - { url = "https://files.pythonhosted.org/packages/fe/c9/b0489a01329ab07a83812d9ebcffe7820a38163c6d9e7da644f926ff877c/jiter-0.13.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe49d3ff6db74321f144dff9addd4a5874d3105ac5ba7c5b77fac099cfae31ae", size = 362904, upload-time = "2026-02-02T12:35:28.925Z" }, - { url = "https://files.pythonhosted.org/packages/05/af/53e561352a44afcba9a9bc67ee1d320b05a370aed8df54eafe714c4e454d/jiter-0.13.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2113c17c9a67071b0f820733c0893ed1d467b5fcf4414068169e5c2cabddb1e2", size = 392380, upload-time = "2026-02-02T12:35:30.385Z" }, - { url = "https://files.pythonhosted.org/packages/76/2a/dd805c3afb8ed5b326c5ae49e725d1b1255b9754b1b77dbecdc621b20773/jiter-0.13.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ab1185ca5c8b9491b55ebf6c1e8866b8f68258612899693e24a92c5fdb9455d5", size = 517939, upload-time = "2026-02-02T12:35:31.865Z" }, - { url = "https://files.pythonhosted.org/packages/20/2a/7b67d76f55b8fe14c937e7640389612f05f9a4145fc28ae128aaa5e62257/jiter-0.13.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:9621ca242547edc16400981ca3231e0c91c0c4c1ab8573a596cd9bb3575d5c2b", size = 551696, upload-time = "2026-02-02T12:35:33.306Z" }, - { url = "https://files.pythonhosted.org/packages/85/9c/57cdd64dac8f4c6ab8f994fe0eb04dc9fd1db102856a4458fcf8a99dfa62/jiter-0.13.0-cp310-cp310-win32.whl", hash = "sha256:a7637d92b1c9d7a771e8c56f445c7f84396d48f2e756e5978840ecba2fac0894", size = 204592, upload-time = "2026-02-02T12:35:34.58Z" }, - { url = "https://files.pythonhosted.org/packages/a7/38/f4f3ea5788b8a5bae7510a678cdc747eda0c45ffe534f9878ff37e7cf3b3/jiter-0.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:c1b609e5cbd2f52bb74fb721515745b407df26d7b800458bd97cb3b972c29e7d", size = 206016, upload-time = "2026-02-02T12:35:36.435Z" }, - { url = "https://files.pythonhosted.org/packages/71/29/499f8c9eaa8a16751b1c0e45e6f5f1761d180da873d417996cc7bddc8eef/jiter-0.13.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:ea026e70a9a28ebbdddcbcf0f1323128a8db66898a06eaad3a4e62d2f554d096", size = 311157, upload-time = "2026-02-02T12:35:37.758Z" }, - { url = "https://files.pythonhosted.org/packages/50/f6/566364c777d2ab450b92100bea11333c64c38d32caf8dc378b48e5b20c46/jiter-0.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:66aa3e663840152d18cc8ff1e4faad3dd181373491b9cfdc6004b92198d67911", size = 319729, upload-time = "2026-02-02T12:35:39.246Z" }, - { url = "https://files.pythonhosted.org/packages/73/dd/560f13ec5e4f116d8ad2658781646cca91b617ae3b8758d4a5076b278f70/jiter-0.13.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c3524798e70655ff19aec58c7d05adb1f074fecff62da857ea9be2b908b6d701", size = 354766, upload-time = "2026-02-02T12:35:40.662Z" }, - { url = "https://files.pythonhosted.org/packages/7c/0d/061faffcfe94608cbc28a0d42a77a74222bdf5055ccdbe5fd2292b94f510/jiter-0.13.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ec7e287d7fbd02cb6e22f9a00dd9c9cd504c40a61f2c61e7e1f9690a82726b4c", size = 362587, upload-time = "2026-02-02T12:35:42.025Z" }, - { url = "https://files.pythonhosted.org/packages/92/c9/c66a7864982fd38a9773ec6e932e0398d1262677b8c60faecd02ffb67bf3/jiter-0.13.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:47455245307e4debf2ce6c6e65a717550a0244231240dcf3b8f7d64e4c2f22f4", size = 487537, upload-time = "2026-02-02T12:35:43.459Z" }, - { url = "https://files.pythonhosted.org/packages/6c/86/84eb4352cd3668f16d1a88929b5888a3fe0418ea8c1dfc2ad4e7bf6e069a/jiter-0.13.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ee9da221dca6e0429c2704c1b3655fe7b025204a71d4d9b73390c759d776d165", size = 373717, upload-time = "2026-02-02T12:35:44.928Z" }, - { url = "https://files.pythonhosted.org/packages/6e/09/9fe4c159358176f82d4390407a03f506a8659ed13ca3ac93a843402acecf/jiter-0.13.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24ab43126d5e05f3d53a36a8e11eb2f23304c6c1117844aaaf9a0aa5e40b5018", size = 362683, upload-time = "2026-02-02T12:35:46.636Z" }, - { url = "https://files.pythonhosted.org/packages/c9/5e/85f3ab9caca0c1d0897937d378b4a515cae9e119730563572361ea0c48ae/jiter-0.13.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9da38b4fedde4fb528c740c2564628fbab737166a0e73d6d46cb4bb5463ff411", size = 392345, upload-time = "2026-02-02T12:35:48.088Z" }, - { url = "https://files.pythonhosted.org/packages/12/4c/05b8629ad546191939e6f0c2f17e29f542a398f4a52fb987bc70b6d1eb8b/jiter-0.13.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0b34c519e17658ed88d5047999a93547f8889f3c1824120c26ad6be5f27b6cf5", size = 517775, upload-time = "2026-02-02T12:35:49.482Z" }, - { url = "https://files.pythonhosted.org/packages/4d/88/367ea2eb6bc582c7052e4baf5ddf57ebe5ab924a88e0e09830dfb585c02d/jiter-0.13.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d2a6394e6af690d462310a86b53c47ad75ac8c21dc79f120714ea449979cb1d3", size = 551325, upload-time = "2026-02-02T12:35:51.104Z" }, - { url = "https://files.pythonhosted.org/packages/f3/12/fa377ffb94a2f28c41afaed093e0d70cfe512035d5ecb0cad0ae4792d35e/jiter-0.13.0-cp311-cp311-win32.whl", hash = "sha256:0f0c065695f616a27c920a56ad0d4fc46415ef8b806bf8fc1cacf25002bd24e1", size = 204709, upload-time = "2026-02-02T12:35:52.467Z" }, - { url = "https://files.pythonhosted.org/packages/cb/16/8e8203ce92f844dfcd3d9d6a5a7322c77077248dbb12da52d23193a839cd/jiter-0.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:0733312953b909688ae3c2d58d043aa040f9f1a6a75693defed7bc2cc4bf2654", size = 204560, upload-time = "2026-02-02T12:35:53.925Z" }, - { url = "https://files.pythonhosted.org/packages/44/26/97cc40663deb17b9e13c3a5cf29251788c271b18ee4d262c8f94798b8336/jiter-0.13.0-cp311-cp311-win_arm64.whl", hash = "sha256:5d9b34ad56761b3bf0fbe8f7e55468704107608512350962d3317ffd7a4382d5", size = 189608, upload-time = "2026-02-02T12:35:55.304Z" }, - { url = "https://files.pythonhosted.org/packages/2e/30/7687e4f87086829955013ca12a9233523349767f69653ebc27036313def9/jiter-0.13.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:0a2bd69fc1d902e89925fc34d1da51b2128019423d7b339a45d9e99c894e0663", size = 307958, upload-time = "2026-02-02T12:35:57.165Z" }, - { url = "https://files.pythonhosted.org/packages/c3/27/e57f9a783246ed95481e6749cc5002a8a767a73177a83c63ea71f0528b90/jiter-0.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f917a04240ef31898182f76a332f508f2cc4b57d2b4d7ad2dbfebbfe167eb505", size = 318597, upload-time = "2026-02-02T12:35:58.591Z" }, - { url = "https://files.pythonhosted.org/packages/cf/52/e5719a60ac5d4d7c5995461a94ad5ef962a37c8bf5b088390e6fad59b2ff/jiter-0.13.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1e2b199f446d3e82246b4fd9236d7cb502dc2222b18698ba0d986d2fecc6152", size = 348821, upload-time = "2026-02-02T12:36:00.093Z" }, - { url = "https://files.pythonhosted.org/packages/61/db/c1efc32b8ba4c740ab3fc2d037d8753f67685f475e26b9d6536a4322bcdd/jiter-0.13.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:04670992b576fa65bd056dbac0c39fe8bd67681c380cb2b48efa885711d9d726", size = 364163, upload-time = "2026-02-02T12:36:01.937Z" }, - { url = "https://files.pythonhosted.org/packages/55/8a/fb75556236047c8806995671a18e4a0ad646ed255276f51a20f32dceaeec/jiter-0.13.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5a1aff1fbdb803a376d4d22a8f63f8e7ccbce0b4890c26cc7af9e501ab339ef0", size = 483709, upload-time = "2026-02-02T12:36:03.41Z" }, - { url = "https://files.pythonhosted.org/packages/7e/16/43512e6ee863875693a8e6f6d532e19d650779d6ba9a81593ae40a9088ff/jiter-0.13.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b3fb8c2053acaef8580809ac1d1f7481a0a0bdc012fd7f5d8b18fb696a5a089", size = 370480, upload-time = "2026-02-02T12:36:04.791Z" }, - { url = "https://files.pythonhosted.org/packages/f8/4c/09b93e30e984a187bc8aaa3510e1ec8dcbdcd71ca05d2f56aac0492453aa/jiter-0.13.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bdaba7d87e66f26a2c45d8cbadcbfc4bf7884182317907baf39cfe9775bb4d93", size = 360735, upload-time = "2026-02-02T12:36:06.994Z" }, - { url = "https://files.pythonhosted.org/packages/1a/1b/46c5e349019874ec5dfa508c14c37e29864ea108d376ae26d90bee238cd7/jiter-0.13.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7b88d649135aca526da172e48083da915ec086b54e8e73a425ba50999468cc08", size = 391814, upload-time = "2026-02-02T12:36:08.368Z" }, - { url = "https://files.pythonhosted.org/packages/15/9e/26184760e85baee7162ad37b7912797d2077718476bf91517641c92b3639/jiter-0.13.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:e404ea551d35438013c64b4f357b0474c7abf9f781c06d44fcaf7a14c69ff9e2", size = 513990, upload-time = "2026-02-02T12:36:09.993Z" }, - { url = "https://files.pythonhosted.org/packages/e9/34/2c9355247d6debad57a0a15e76ab1566ab799388042743656e566b3b7de1/jiter-0.13.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1f4748aad1b4a93c8bdd70f604d0f748cdc0e8744c5547798acfa52f10e79228", size = 548021, upload-time = "2026-02-02T12:36:11.376Z" }, - { url = "https://files.pythonhosted.org/packages/ac/4a/9f2c23255d04a834398b9c2e0e665382116911dc4d06b795710503cdad25/jiter-0.13.0-cp312-cp312-win32.whl", hash = "sha256:0bf670e3b1445fc4d31612199f1744f67f889ee1bbae703c4b54dc097e5dd394", size = 203024, upload-time = "2026-02-02T12:36:12.682Z" }, - { url = "https://files.pythonhosted.org/packages/09/ee/f0ae675a957ae5a8f160be3e87acea6b11dc7b89f6b7ab057e77b2d2b13a/jiter-0.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:15db60e121e11fe186c0b15236bd5d18381b9ddacdcf4e659feb96fc6c969c92", size = 205424, upload-time = "2026-02-02T12:36:13.93Z" }, - { url = "https://files.pythonhosted.org/packages/1b/02/ae611edf913d3cbf02c97cdb90374af2082c48d7190d74c1111dde08bcdd/jiter-0.13.0-cp312-cp312-win_arm64.whl", hash = "sha256:41f92313d17989102f3cb5dd533a02787cdb99454d494344b0361355da52fcb9", size = 186818, upload-time = "2026-02-02T12:36:15.308Z" }, - { url = "https://files.pythonhosted.org/packages/91/9c/7ee5a6ff4b9991e1a45263bfc46731634c4a2bde27dfda6c8251df2d958c/jiter-0.13.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1f8a55b848cbabf97d861495cd65f1e5c590246fabca8b48e1747c4dfc8f85bf", size = 306897, upload-time = "2026-02-02T12:36:16.748Z" }, - { url = "https://files.pythonhosted.org/packages/7c/02/be5b870d1d2be5dd6a91bdfb90f248fbb7dcbd21338f092c6b89817c3dbf/jiter-0.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f556aa591c00f2c45eb1b89f68f52441a016034d18b65da60e2d2875bbbf344a", size = 317507, upload-time = "2026-02-02T12:36:18.351Z" }, - { url = "https://files.pythonhosted.org/packages/da/92/b25d2ec333615f5f284f3a4024f7ce68cfa0604c322c6808b2344c7f5d2b/jiter-0.13.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f7e1d61da332ec412350463891923f960c3073cf1aae93b538f0bb4c8cd46efb", size = 350560, upload-time = "2026-02-02T12:36:19.746Z" }, - { url = "https://files.pythonhosted.org/packages/be/ec/74dcb99fef0aca9fbe56b303bf79f6bd839010cb18ad41000bf6cc71eec0/jiter-0.13.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3097d665a27bc96fd9bbf7f86178037db139f319f785e4757ce7ccbf390db6c2", size = 363232, upload-time = "2026-02-02T12:36:21.243Z" }, - { url = "https://files.pythonhosted.org/packages/1b/37/f17375e0bb2f6a812d4dd92d7616e41917f740f3e71343627da9db2824ce/jiter-0.13.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d01ecc3a8cbdb6f25a37bd500510550b64ddf9f7d64a107d92f3ccb25035d0f", size = 483727, upload-time = "2026-02-02T12:36:22.688Z" }, - { url = "https://files.pythonhosted.org/packages/77/d2/a71160a5ae1a1e66c1395b37ef77da67513b0adba73b993a27fbe47eb048/jiter-0.13.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ed9bbc30f5d60a3bdf63ae76beb3f9db280d7f195dfcfa61af792d6ce912d159", size = 370799, upload-time = "2026-02-02T12:36:24.106Z" }, - { url = "https://files.pythonhosted.org/packages/01/99/ed5e478ff0eb4e8aa5fd998f9d69603c9fd3f32de3bd16c2b1194f68361c/jiter-0.13.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98fbafb6e88256f4454de33c1f40203d09fc33ed19162a68b3b257b29ca7f663", size = 359120, upload-time = "2026-02-02T12:36:25.519Z" }, - { url = "https://files.pythonhosted.org/packages/16/be/7ffd08203277a813f732ba897352797fa9493faf8dc7995b31f3d9cb9488/jiter-0.13.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5467696f6b827f1116556cb0db620440380434591e93ecee7fd14d1a491b6daa", size = 390664, upload-time = "2026-02-02T12:36:26.866Z" }, - { url = "https://files.pythonhosted.org/packages/d1/84/e0787856196d6d346264d6dcccb01f741e5f0bd014c1d9a2ebe149caf4f3/jiter-0.13.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:2d08c9475d48b92892583df9da592a0e2ac49bcd41fae1fec4f39ba6cf107820", size = 513543, upload-time = "2026-02-02T12:36:28.217Z" }, - { url = "https://files.pythonhosted.org/packages/65/50/ecbd258181c4313cf79bca6c88fb63207d04d5bf5e4f65174114d072aa55/jiter-0.13.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:aed40e099404721d7fcaf5b89bd3b4568a4666358bcac7b6b15c09fb6252ab68", size = 547262, upload-time = "2026-02-02T12:36:29.678Z" }, - { url = "https://files.pythonhosted.org/packages/27/da/68f38d12e7111d2016cd198161b36e1f042bd115c169255bcb7ec823a3bf/jiter-0.13.0-cp313-cp313-win32.whl", hash = "sha256:36ebfbcffafb146d0e6ffb3e74d51e03d9c35ce7c625c8066cdbfc7b953bdc72", size = 200630, upload-time = "2026-02-02T12:36:31.808Z" }, - { url = "https://files.pythonhosted.org/packages/25/65/3bd1a972c9a08ecd22eb3b08a95d1941ebe6938aea620c246cf426ae09c2/jiter-0.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:8d76029f077379374cf0dbc78dbe45b38dec4a2eb78b08b5194ce836b2517afc", size = 202602, upload-time = "2026-02-02T12:36:33.679Z" }, - { url = "https://files.pythonhosted.org/packages/15/fe/13bd3678a311aa67686bb303654792c48206a112068f8b0b21426eb6851e/jiter-0.13.0-cp313-cp313-win_arm64.whl", hash = "sha256:bb7613e1a427cfcb6ea4544f9ac566b93d5bf67e0d48c787eca673ff9c9dff2b", size = 185939, upload-time = "2026-02-02T12:36:35.065Z" }, - { url = "https://files.pythonhosted.org/packages/49/19/a929ec002ad3228bc97ca01dbb14f7632fffdc84a95ec92ceaf4145688ae/jiter-0.13.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fa476ab5dd49f3bf3a168e05f89358c75a17608dbabb080ef65f96b27c19ab10", size = 316616, upload-time = "2026-02-02T12:36:36.579Z" }, - { url = "https://files.pythonhosted.org/packages/52/56/d19a9a194afa37c1728831e5fb81b7722c3de18a3109e8f282bfc23e587a/jiter-0.13.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ade8cb6ff5632a62b7dbd4757d8c5573f7a2e9ae285d6b5b841707d8363205ef", size = 346850, upload-time = "2026-02-02T12:36:38.058Z" }, - { url = "https://files.pythonhosted.org/packages/36/4a/94e831c6bf287754a8a019cb966ed39ff8be6ab78cadecf08df3bb02d505/jiter-0.13.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9950290340acc1adaded363edd94baebcee7dabdfa8bee4790794cd5cfad2af6", size = 358551, upload-time = "2026-02-02T12:36:39.417Z" }, - { url = "https://files.pythonhosted.org/packages/a2/ec/a4c72c822695fa80e55d2b4142b73f0012035d9fcf90eccc56bc060db37c/jiter-0.13.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2b4972c6df33731aac0742b64fd0d18e0a69bc7d6e03108ce7d40c85fd9e3e6d", size = 201950, upload-time = "2026-02-02T12:36:40.791Z" }, - { url = "https://files.pythonhosted.org/packages/b6/00/393553ec27b824fbc29047e9c7cd4a3951d7fbe4a76743f17e44034fa4e4/jiter-0.13.0-cp313-cp313t-win_arm64.whl", hash = "sha256:701a1e77d1e593c1b435315ff625fd071f0998c5f02792038a5ca98899261b7d", size = 185852, upload-time = "2026-02-02T12:36:42.077Z" }, - { url = "https://files.pythonhosted.org/packages/6e/f5/f1997e987211f6f9bd71b8083047b316208b4aca0b529bb5f8c96c89ef3e/jiter-0.13.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:cc5223ab19fe25e2f0bf2643204ad7318896fe3729bf12fde41b77bfc4fafff0", size = 308804, upload-time = "2026-02-02T12:36:43.496Z" }, - { url = "https://files.pythonhosted.org/packages/cd/8f/5482a7677731fd44881f0204981ce2d7175db271f82cba2085dd2212e095/jiter-0.13.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9776ebe51713acf438fd9b4405fcd86893ae5d03487546dae7f34993217f8a91", size = 318787, upload-time = "2026-02-02T12:36:45.071Z" }, - { url = "https://files.pythonhosted.org/packages/f3/b9/7257ac59778f1cd025b26a23c5520a36a424f7f1b068f2442a5b499b7464/jiter-0.13.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:879e768938e7b49b5e90b7e3fecc0dbec01b8cb89595861fb39a8967c5220d09", size = 353880, upload-time = "2026-02-02T12:36:47.365Z" }, - { url = "https://files.pythonhosted.org/packages/c3/87/719eec4a3f0841dad99e3d3604ee4cba36af4419a76f3cb0b8e2e691ad67/jiter-0.13.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:682161a67adea11e3aae9038c06c8b4a9a71023228767477d683f69903ebc607", size = 366702, upload-time = "2026-02-02T12:36:48.871Z" }, - { url = "https://files.pythonhosted.org/packages/d2/65/415f0a75cf6921e43365a1bc227c565cb949caca8b7532776e430cbaa530/jiter-0.13.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a13b68cd1cd8cc9de8f244ebae18ccb3e4067ad205220ef324c39181e23bbf66", size = 486319, upload-time = "2026-02-02T12:36:53.006Z" }, - { url = "https://files.pythonhosted.org/packages/54/a2/9e12b48e82c6bbc6081fd81abf915e1443add1b13d8fc586e1d90bb02bb8/jiter-0.13.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:87ce0f14c6c08892b610686ae8be350bf368467b6acd5085a5b65441e2bf36d2", size = 372289, upload-time = "2026-02-02T12:36:54.593Z" }, - { url = "https://files.pythonhosted.org/packages/4e/c1/e4693f107a1789a239c759a432e9afc592366f04e901470c2af89cfd28e1/jiter-0.13.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c365005b05505a90d1c47856420980d0237adf82f70c4aff7aebd3c1cc143ad", size = 360165, upload-time = "2026-02-02T12:36:56.112Z" }, - { url = "https://files.pythonhosted.org/packages/17/08/91b9ea976c1c758240614bd88442681a87672eebc3d9a6dde476874e706b/jiter-0.13.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1317fdffd16f5873e46ce27d0e0f7f4f90f0cdf1d86bf6abeaea9f63ca2c401d", size = 389634, upload-time = "2026-02-02T12:36:57.495Z" }, - { url = "https://files.pythonhosted.org/packages/18/23/58325ef99390d6d40427ed6005bf1ad54f2577866594bcf13ce55675f87d/jiter-0.13.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:c05b450d37ba0c9e21c77fef1f205f56bcee2330bddca68d344baebfc55ae0df", size = 514933, upload-time = "2026-02-02T12:36:58.909Z" }, - { url = "https://files.pythonhosted.org/packages/5b/25/69f1120c7c395fd276c3996bb8adefa9c6b84c12bb7111e5c6ccdcd8526d/jiter-0.13.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:775e10de3849d0631a97c603f996f518159272db00fdda0a780f81752255ee9d", size = 548842, upload-time = "2026-02-02T12:37:00.433Z" }, - { url = "https://files.pythonhosted.org/packages/18/05/981c9669d86850c5fbb0d9e62bba144787f9fba84546ba43d624ee27ef29/jiter-0.13.0-cp314-cp314-win32.whl", hash = "sha256:632bf7c1d28421c00dd8bbb8a3bac5663e1f57d5cd5ed962bce3c73bf62608e6", size = 202108, upload-time = "2026-02-02T12:37:01.718Z" }, - { url = "https://files.pythonhosted.org/packages/8d/96/cdcf54dd0b0341db7d25413229888a346c7130bd20820530905fdb65727b/jiter-0.13.0-cp314-cp314-win_amd64.whl", hash = "sha256:f22ef501c3f87ede88f23f9b11e608581c14f04db59b6a801f354397ae13739f", size = 204027, upload-time = "2026-02-02T12:37:03.075Z" }, - { url = "https://files.pythonhosted.org/packages/fb/f9/724bcaaab7a3cd727031fe4f6995cb86c4bd344909177c186699c8dec51a/jiter-0.13.0-cp314-cp314-win_arm64.whl", hash = "sha256:07b75fe09a4ee8e0c606200622e571e44943f47254f95e2436c8bdcaceb36d7d", size = 187199, upload-time = "2026-02-02T12:37:04.414Z" }, - { url = "https://files.pythonhosted.org/packages/62/92/1661d8b9fd6a3d7a2d89831db26fe3c1509a287d83ad7838831c7b7a5c7e/jiter-0.13.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:964538479359059a35fb400e769295d4b315ae61e4105396d355a12f7fef09f0", size = 318423, upload-time = "2026-02-02T12:37:05.806Z" }, - { url = "https://files.pythonhosted.org/packages/4f/3b/f77d342a54d4ebcd128e520fc58ec2f5b30a423b0fd26acdfc0c6fef8e26/jiter-0.13.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e104da1db1c0991b3eaed391ccd650ae8d947eab1480c733e5a3fb28d4313e40", size = 351438, upload-time = "2026-02-02T12:37:07.189Z" }, - { url = "https://files.pythonhosted.org/packages/76/b3/ba9a69f0e4209bd3331470c723c2f5509e6f0482e416b612431a5061ed71/jiter-0.13.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0e3a5f0cde8ff433b8e88e41aa40131455420fb3649a3c7abdda6145f8cb7202", size = 364774, upload-time = "2026-02-02T12:37:08.579Z" }, - { url = "https://files.pythonhosted.org/packages/b3/16/6cdb31fa342932602458dbb631bfbd47f601e03d2e4950740e0b2100b570/jiter-0.13.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:57aab48f40be1db920a582b30b116fe2435d184f77f0e4226f546794cedd9cf0", size = 487238, upload-time = "2026-02-02T12:37:10.066Z" }, - { url = "https://files.pythonhosted.org/packages/ed/b1/956cc7abaca8d95c13aa8d6c9b3f3797241c246cd6e792934cc4c8b250d2/jiter-0.13.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7772115877c53f62beeb8fd853cab692dbc04374ef623b30f997959a4c0e7e95", size = 372892, upload-time = "2026-02-02T12:37:11.656Z" }, - { url = "https://files.pythonhosted.org/packages/26/c4/97ecde8b1e74f67b8598c57c6fccf6df86ea7861ed29da84629cdbba76c4/jiter-0.13.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1211427574b17b633cfceba5040de8081e5abf114f7a7602f73d2e16f9fdaa59", size = 360309, upload-time = "2026-02-02T12:37:13.244Z" }, - { url = "https://files.pythonhosted.org/packages/4b/d7/eabe3cf46715854ccc80be2cd78dd4c36aedeb30751dbf85a1d08c14373c/jiter-0.13.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7beae3a3d3b5212d3a55d2961db3c292e02e302feb43fce6a3f7a31b90ea6dfe", size = 389607, upload-time = "2026-02-02T12:37:14.881Z" }, - { url = "https://files.pythonhosted.org/packages/df/2d/03963fc0804e6109b82decfb9974eb92df3797fe7222428cae12f8ccaa0c/jiter-0.13.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:e5562a0f0e90a6223b704163ea28e831bd3a9faa3512a711f031611e6b06c939", size = 514986, upload-time = "2026-02-02T12:37:16.326Z" }, - { url = "https://files.pythonhosted.org/packages/f6/6c/8c83b45eb3eb1c1e18d841fe30b4b5bc5619d781267ca9bc03e005d8fd0a/jiter-0.13.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:6c26a424569a59140fb51160a56df13f438a2b0967365e987889186d5fc2f6f9", size = 548756, upload-time = "2026-02-02T12:37:17.736Z" }, - { url = "https://files.pythonhosted.org/packages/47/66/eea81dfff765ed66c68fd2ed8c96245109e13c896c2a5015c7839c92367e/jiter-0.13.0-cp314-cp314t-win32.whl", hash = "sha256:24dc96eca9f84da4131cdf87a95e6ce36765c3b156fc9ae33280873b1c32d5f6", size = 201196, upload-time = "2026-02-02T12:37:19.101Z" }, - { url = "https://files.pythonhosted.org/packages/ff/32/4ac9c7a76402f8f00d00842a7f6b83b284d0cf7c1e9d4227bc95aa6d17fa/jiter-0.13.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0a8d76c7524087272c8ae913f5d9d608bd839154b62c4322ef65723d2e5bb0b8", size = 204215, upload-time = "2026-02-02T12:37:20.495Z" }, - { url = "https://files.pythonhosted.org/packages/f9/8e/7def204fea9f9be8b3c21a6f2dd6c020cf56c7d5ff753e0e23ed7f9ea57e/jiter-0.13.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2c26cf47e2cad140fa23b6d58d435a7c0161f5c514284802f25e87fddfe11024", size = 187152, upload-time = "2026-02-02T12:37:22.124Z" }, - { url = "https://files.pythonhosted.org/packages/79/b3/3c29819a27178d0e461a8571fb63c6ae38be6dc36b78b3ec2876bbd6a910/jiter-0.13.0-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b1cbfa133241d0e6bdab48dcdc2604e8ba81512f6bbd68ec3e8e1357dd3c316c", size = 307016, upload-time = "2026-02-02T12:37:42.755Z" }, - { url = "https://files.pythonhosted.org/packages/eb/ae/60993e4b07b1ac5ebe46da7aa99fdbb802eb986c38d26e3883ac0125c4e0/jiter-0.13.0-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:db367d8be9fad6e8ebbac4a7578b7af562e506211036cba2c06c3b998603c3d2", size = 305024, upload-time = "2026-02-02T12:37:44.774Z" }, - { url = "https://files.pythonhosted.org/packages/77/fa/2227e590e9cf98803db2811f172b2d6460a21539ab73006f251c66f44b14/jiter-0.13.0-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45f6f8efb2f3b0603092401dc2df79fa89ccbc027aaba4174d2d4133ed661434", size = 339337, upload-time = "2026-02-02T12:37:46.668Z" }, - { url = "https://files.pythonhosted.org/packages/2d/92/015173281f7eb96c0ef580c997da8ef50870d4f7f4c9e03c845a1d62ae04/jiter-0.13.0-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:597245258e6ad085d064780abfb23a284d418d3e61c57362d9449c6c7317ee2d", size = 346395, upload-time = "2026-02-02T12:37:48.09Z" }, - { url = "https://files.pythonhosted.org/packages/80/60/e50fa45dd7e2eae049f0ce964663849e897300433921198aef94b6ffa23a/jiter-0.13.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:3d744a6061afba08dd7ae375dcde870cffb14429b7477e10f67e9e6d68772a0a", size = 305169, upload-time = "2026-02-02T12:37:50.376Z" }, - { url = "https://files.pythonhosted.org/packages/d2/73/a009f41c5eed71c49bec53036c4b33555afcdee70682a18c6f66e396c039/jiter-0.13.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:ff732bd0a0e778f43d5009840f20b935e79087b4dc65bd36f1cd0f9b04b8ff7f", size = 303808, upload-time = "2026-02-02T12:37:52.092Z" }, - { url = "https://files.pythonhosted.org/packages/c4/10/528b439290763bff3d939268085d03382471b442f212dca4ff5f12802d43/jiter-0.13.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab44b178f7981fcaea7e0a5df20e773c663d06ffda0198f1a524e91b2fde7e59", size = 337384, upload-time = "2026-02-02T12:37:53.582Z" }, - { url = "https://files.pythonhosted.org/packages/67/8a/a342b2f0251f3dac4ca17618265d93bf244a2a4d089126e81e4c1056ac50/jiter-0.13.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7bb00b6d26db67a05fe3e12c76edc75f32077fb51deed13822dc648fa373bc19", size = 343768, upload-time = "2026-02-02T12:37:55.055Z" }, + { url = "https://files.pythonhosted.org/packages/76/d8/b959609e44012a42b1f3e5ba98ea3b33c7e41e6d4b77cd8f00fd19b1d3ad/jiter-0.16.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:c5fc4f8def331036a7b8e981b4347ebe409981edbc8308a5ea842b8c3614fa6c", size = 310082, upload-time = "2026-06-29T13:02:31.356Z" }, + { url = "https://files.pythonhosted.org/packages/c6/3d/4d7f5667ea0e0548534ba880b84bb3d12924fd133aa83ad6c6c80fca3d76/jiter-0.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5a71d0d2014c3275043e1170bf3d4e771493cb0dcf07be54c567155f4d8ee64b", size = 315643, upload-time = "2026-06-29T13:02:33.204Z" }, + { url = "https://files.pythonhosted.org/packages/9b/83/bed2dcb5c9f3e1ccfcbc67dda48265fe7d5ad0c9cadda5fe95f6e3b87f94/jiter-0.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:741eed508c233a76313a1c7b001f8f21b82f14327e9196ae8bd29a2cc164ae84", size = 341363, upload-time = "2026-06-29T13:02:34.853Z" }, + { url = "https://files.pythonhosted.org/packages/f4/2f/6bb3c3dda668ebc0445689c81a2b0f26a82b10843d67ed9c9b2c3edc177f/jiter-0.16.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3fb7bc819187b56dc48aa5c833aaf92257da8e07efdb9306156667bd2eeb491c", size = 365483, upload-time = "2026-06-29T13:02:36.295Z" }, + { url = "https://files.pythonhosted.org/packages/92/35/8a045ccb39164e70dcdae696413b661771f148b68b12b175c3a04d901937/jiter-0.16.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7c9610fd25ebccb43fca584136f5c2fbb26802447eccd430dfdbab95a0fd5126", size = 461219, upload-time = "2026-06-29T13:02:38.116Z" }, + { url = "https://files.pythonhosted.org/packages/e7/99/22292dbbf0ed0c610cfe5ddc7f3bd67237a412f121318f865196e62a07bd/jiter-0.16.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4a1d68ff7ca1d3b5dee20a97a3decda7d5f15003823bf6d140c81f8561d3bc5c", size = 374905, upload-time = "2026-06-29T13:02:40.357Z" }, + { url = "https://files.pythonhosted.org/packages/29/ac/2f55ccb1f0eeafa6d89d24caf52f6f0944a59290ee199e9ade62177dca42/jiter-0.16.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb08c276dd02dac3a284acdd02cacc630d2e3cd6572a4b85519f35cbd133c3de", size = 348320, upload-time = "2026-06-29T13:02:41.923Z" }, + { url = "https://files.pythonhosted.org/packages/50/e3/7d88b9174c40064fabc07c84a9b62e6b10f5644562ec0e0a29392edbe978/jiter-0.16.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:8fc4d94713c4697347e38faf7d6ef91547c142219bdcfc7220c4870879974244", size = 356519, upload-time = "2026-06-29T13:02:43.436Z" }, + { url = "https://files.pythonhosted.org/packages/27/57/c4a33aeef513a9d5e26e31534e0bcc752d6ea0e54c94ddb7b68bade669c2/jiter-0.16.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a0f05e229edb29e68cdd0ccb83cea13b64263416120cf943767a6fd72e6787f", size = 394204, upload-time = "2026-06-29T13:02:44.987Z" }, + { url = "https://files.pythonhosted.org/packages/9d/70/c6c23e76ebb3766b111bc399437bbc9f870a76e2a92e10b2a5f561d57372/jiter-0.16.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:2c842cbf374a8daf50b2c04212995bee34ca2ac2cdc29a901b4cdb072c9c4131", size = 521477, upload-time = "2026-06-29T13:02:46.724Z" }, + { url = "https://files.pythonhosted.org/packages/2a/d3/0001c8c0c5976af2625bb1cfb1895e8ec693b6589fe4574b8e6fc2c85501/jiter-0.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:5ed466aee31294d7cdcd4d37dfe5c42c97bc29d9a5f00eacf24504358309cb9b", size = 552187, upload-time = "2026-06-29T13:02:48.144Z" }, + { url = "https://files.pythonhosted.org/packages/f6/76/311b718e07e85740e48619c0632b36f7e0b8d113984499e436452ed13a9a/jiter-0.16.0-cp310-cp310-win32.whl", hash = "sha256:b42e9ff5376819c053da25809a8d4b6fa6e473b4856ebe42e298ac958be3d7f9", size = 206513, upload-time = "2026-06-29T13:02:49.515Z" }, + { url = "https://files.pythonhosted.org/packages/db/7f/ac680eeb0777dc0eb7dc824800ba27880d7f6bc712e362d34ad8ee559f36/jiter-0.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:10438939205546132189c8e74a2d536a707841f3a25cd7c74ee91fe503407a26", size = 199505, upload-time = "2026-06-29T13:02:50.829Z" }, + { url = "https://files.pythonhosted.org/packages/4e/3f/fae6cc967d120ec89e31c5418a51176d8278b3087fbb384a9176754f353c/jiter-0.16.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:67fddeda1688f0cce2d2ae83ccf8a80f79936f2d2997d6cc2261f82fdb54a4d3", size = 309289, upload-time = "2026-06-29T13:02:52.301Z" }, + { url = "https://files.pythonhosted.org/packages/c8/e3/97c6c3562c077f6247d6e6ce5c82562500b6316c0d928e97e106b7a1321a/jiter-0.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c90c0f63df322be920eda6ce622e3083d8906ba267f8220fe7873213b8b4430e", size = 315181, upload-time = "2026-06-29T13:02:53.964Z" }, + { url = "https://files.pythonhosted.org/packages/7b/89/d8d073f8aa2667e46c6c0873f86fe4a512bba4293cc730f626a076211a62/jiter-0.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:64c0203212098470032aabcde9356fc168f377aade3e43def61dfe17e92f2037", size = 340939, upload-time = "2026-06-29T13:02:55.412Z" }, + { url = "https://files.pythonhosted.org/packages/87/c9/db4fda3ed73fb864139305e935e5b8b38a5a24692a5a9dd356c22f1b9c8d/jiter-0.16.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:12288303c9844e61e1651d02a9a6f6633e47d39f897d6991d1427161ce6b746e", size = 364932, upload-time = "2026-06-29T13:02:57.28Z" }, + { url = "https://files.pythonhosted.org/packages/a2/74/52b5e86241057f52ddd7c9a580f90effb51f9d06239f6fc612279b91a838/jiter-0.16.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cf109d010b4b05a105afb3d43be36a21322d345ad3111e13d15f680afef0e5b", size = 461132, upload-time = "2026-06-29T13:02:58.994Z" }, + { url = "https://files.pythonhosted.org/packages/a9/87/544a700f7447c1f31c5d7833821a4daa5683165c2d5a094fbf5b5800c3dc/jiter-0.16.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:62c1b7fe1f77925acf5af68b6140b8810fa87dfd4dc0a9c8568ec2fa2a10429c", size = 374857, upload-time = "2026-06-29T13:03:00.455Z" }, + { url = "https://files.pythonhosted.org/packages/40/cd/0fcc3f7d39183674d5bfa9ec640faaeb506c60be7c8f94625dfba366e37c/jiter-0.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8597d23c87f59294f83bcb6229b9ed1fccee13dbba967b46930d2f1759466fee", size = 347053, upload-time = "2026-06-29T13:03:02.045Z" }, + { url = "https://files.pythonhosted.org/packages/5c/ae/c7e64e7932ad597fa395b61440b249ada6366716e25c6e08dd2afbd021e6/jiter-0.16.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:3126a5dbad56401989ac769aca0cb56005bfb3e2366eea0ca99d1a91c3c1ee03", size = 356153, upload-time = "2026-06-29T13:03:03.706Z" }, + { url = "https://files.pythonhosted.org/packages/d4/1c/1c719044f14da814e1a060191ab19b96f3e99207bc5b4bfc6d6be34b3f80/jiter-0.16.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c4b4717bdb35ae456f831a6b08d01880fff399887a6bbc526a583a406e484eea", size = 393956, upload-time = "2026-06-29T13:03:05.165Z" }, + { url = "https://files.pythonhosted.org/packages/3b/dc/7b2f303a2847207e265503853a2d964a55354cffd62a5f2936c155486798/jiter-0.16.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:adff21bc78edfe086c15eb495b900306076de378dc2337c132401fc39bd79c91", size = 521081, upload-time = "2026-06-29T13:03:06.886Z" }, + { url = "https://files.pythonhosted.org/packages/c2/5f/501cf6e1e09caeb420195179ffc6f62aca603f1220ec53fd80d0d70b3e56/jiter-0.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:dab907db06fc593645e73109acf4581ba5b548897d28b9348dc41ddc8343b2d3", size = 552085, upload-time = "2026-06-29T13:03:08.339Z" }, + { url = "https://files.pythonhosted.org/packages/79/54/aa5be86520113b79455c3877f3d1f07a348098df4083ba3688e9537e52dd/jiter-0.16.0-cp311-cp311-win32.whl", hash = "sha256:560b2cf3fb03240cd34f27409a238547488708f05b7c3924f571a60422251ec7", size = 206755, upload-time = "2026-06-29T13:03:09.653Z" }, + { url = "https://files.pythonhosted.org/packages/64/ec/2feb893eb330bd69b413866f4d5daada33c3962f1c6f270c91ca2d87fdf9/jiter-0.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:e431cfc9caf44c1d5459ff77d4e64cbf85fddb6a35dad836a15c6a9ec23087c1", size = 199155, upload-time = "2026-06-29T13:03:10.979Z" }, + { url = "https://files.pythonhosted.org/packages/b9/9c/ca040d94415048a3666fc237774df8151c96f8d2b661cbe3b184acc95876/jiter-0.16.0-cp311-cp311-win_arm64.whl", hash = "sha256:2a8e9e39cf083016137aa5cadafe3188adc2ba6ba1fbf1e5d18889ad3e9ad056", size = 194403, upload-time = "2026-06-29T13:03:12.341Z" }, + { url = "https://files.pythonhosted.org/packages/83/2b/52ace16ed031354f0539749a49e4bf33797d82bea5137910835fa4b09793/jiter-0.16.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:67c3bc1760f8c99d805dcab4e644027142a53b1d5d861f18780ebdbd5d40b72a", size = 306943, upload-time = "2026-06-29T13:03:14.035Z" }, + { url = "https://files.pythonhosted.org/packages/94/2e/34957c2c1b661c252ba9bcc60ae0bddc27e0f7202c6073326a13c5390eec/jiter-0.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5af7780e4a26bd7d0d989592bf9ef12ebf806b74ab709223ecca37c749872ea9", size = 307779, upload-time = "2026-06-29T13:03:15.418Z" }, + { url = "https://files.pythonhosted.org/packages/88/6c/59bd309cab4460c54cf1079f3eb7fe7af6a4c895c5c957a53378693bad2b/jiter-0.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5bf78d0e05e45cfdd66558893938d59afe3d1b1a824a202039b20e607d25a72", size = 335826, upload-time = "2026-06-29T13:03:17.11Z" }, + { url = "https://files.pythonhosted.org/packages/3b/8c/f5ef7b65f0df47afa16596969defb281ebb86e96df346d62be6fd853d620/jiter-0.16.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f4444a83f946605990c98f625cdd3d2725bfb818158760c5748c653170a20e0e", size = 362573, upload-time = "2026-06-29T13:03:18.781Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0b/ace4354da061ee38844a0c27dc2c21eecd27aea119e8da324bea987522d0/jiter-0.16.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3a23f0e4f957e1be65752d2dfac9a5a06b1917af8dc85deb639c3b9d02e31290", size = 457979, upload-time = "2026-06-29T13:03:20.293Z" }, + { url = "https://files.pythonhosted.org/packages/55/40/c0253d3772eb9dcd8e6606ee9b2d53ec8e5b814589c47f140aa585f21eaa/jiter-0.16.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c22a488f7b9218e245a0025a9ba6b100e2e54700831cf4cf16833a27fba3ad01", size = 372302, upload-time = "2026-06-29T13:03:21.739Z" }, + { url = "https://files.pythonhosted.org/packages/a8/d2/4839422241aa12860ce597b20068727094ba0bc480723c74924ca5bad483/jiter-0.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46add52f4ad47a08bfb1219f3e673da972191489a33016edefdb5ea55bfa8c48", size = 343805, upload-time = "2026-06-29T13:03:23.384Z" }, + { url = "https://files.pythonhosted.org/packages/e2/59/e196888a05befdda7dbe299b722d56f2f6eec65402bc34c0a3306d595feb/jiter-0.16.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:9c8a956fd72c2cf1e730d01ea080341f13aa0a97a4a33b51abebe725b7ae9ca9", size = 351107, upload-time = "2026-06-29T13:03:24.815Z" }, + { url = "https://files.pythonhosted.org/packages/ec/74/4cd9e0fca65232136400354b630fbfcd2de634e22ccbb96567725981b548/jiter-0.16.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:561926e0573ffe4a32498420a76d64b16c513e1ab413b9d28158a8764ac701e5", size = 388441, upload-time = "2026-06-29T13:03:26.266Z" }, + { url = "https://files.pythonhosted.org/packages/d9/8c/554691e48bc711299c0a293dd8a6179e24b2d66a54dc295421fcf64569c0/jiter-0.16.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:44d019fa8cdaf89bf29c71b39e3712143fdd0ac76725c6ef954f9957a5ea8730", size = 516354, upload-time = "2026-06-29T13:03:28.02Z" }, + { url = "https://files.pythonhosted.org/packages/a4/cb/01e9d69dc2cc6759d4f91e230b34489c4fdb2518992650633f9e20bece89/jiter-0.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:0df91907609837f33341b8e6fe73b95991fdaa57caf1a0fbd343dffe826f386f", size = 547880, upload-time = "2026-06-29T13:03:29.534Z" }, + { url = "https://files.pythonhosted.org/packages/79/70/2953195f1c6ad00f49fa67e13df7e60acb3dd4f387101bc15abccddd905e/jiter-0.16.0-cp312-cp312-win32.whl", hash = "sha256:51d7b836acb0108d7c77df1742332cac2a1fa04a74d6dacec46e7091f0e91274", size = 203473, upload-time = "2026-06-29T13:03:31.025Z" }, + { url = "https://files.pythonhosted.org/packages/2d/05/2909a8b10699a4d560f8c502b6b2c5f3991b682b1922c1eedda242b225bd/jiter-0.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:1878349266f8ee36ecb1375cc5ba2f115f35fd9f0a1a4119e725e379126647f7", size = 196905, upload-time = "2026-06-29T13:03:32.472Z" }, + { url = "https://files.pythonhosted.org/packages/e9/a9/6b82bb1c8d7790d602489b967b982a909e5d092875a6c2ade96444c8dfc5/jiter-0.16.0-cp312-cp312-win_arm64.whl", hash = "sha256:2ed5738ae4af18271a51a528b8811b0cbfa4a1858de9d83359e4169855d6a331", size = 190618, upload-time = "2026-06-29T13:03:34.672Z" }, + { url = "https://files.pythonhosted.org/packages/91/c0/555fc60473d30d66894ba825e63615e3be7524fac23858356afa7a38906c/jiter-0.16.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:41977aa5654023948c2dae2a81cbf9c43343954bef1cd59a154dd15a4d84c195", size = 306203, upload-time = "2026-06-29T13:03:36.243Z" }, + { url = "https://files.pythonhosted.org/packages/d0/2b/c3eaf16f5d7c9bad66ea32f40a95bd169b29a91217fcc7f081375157e99c/jiter-0.16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d28bb3c26762358dadf3e5bf0bccd29ae987d65e6988d2e6f49829c76b003c09", size = 306489, upload-time = "2026-06-29T13:03:37.846Z" }, + { url = "https://files.pythonhosted.org/packages/96/3f/02fdfc6705cad96127d883af5c34e4867f554f29ec7705ec1a46156400a9/jiter-0.16.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0542a7189c26920778658fc8fcf2af8bae05bae9924577f71804acef37996536", size = 335453, upload-time = "2026-06-29T13:03:39.221Z" }, + { url = "https://files.pythonhosted.org/packages/b2/a6/e4bda5920d4b0d7c5dfb7174ce4a6b2e4d3e11c9162c452ef0eab4cdbdbd/jiter-0.16.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8fb8de1e23a0cb2a7f53c335049c7b72b6db41aa6227cdcc0972a1de5cb39450", size = 361625, upload-time = "2026-06-29T13:03:40.597Z" }, + { url = "https://files.pythonhosted.org/packages/b7/97/4e6b59b2c6e55cbb3e183595f81ad65dcfb21c915fee5e19e335df21bc55/jiter-0.16.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b72d0b2990ca754a9102779ac98d8597b7cb31678958562214a007f909eab78e", size = 456958, upload-time = "2026-06-29T13:03:42.074Z" }, + { url = "https://files.pythonhosted.org/packages/15/e0/97e9557686d2f94f4b93786eccb7eed28e9228ad132ea8237f44727314a7/jiter-0.16.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5f91b1c27fc22a57993d5a5cb8a627cb8ed4b10502716fac1ffbfe1d19d84e8", size = 372017, upload-time = "2026-06-29T13:03:43.658Z" }, + { url = "https://files.pythonhosted.org/packages/0f/94/db768b6938e0df35c86beeba3dfbbb025c9ee5c19e1aa271f2396e50864d/jiter-0.16.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c682bea068a90b764577bdb78a60a4c1d1606daf9cd4c893832a37c7cc9d9026", size = 343320, upload-time = "2026-06-29T13:03:45.226Z" }, + { url = "https://files.pythonhosted.org/packages/c1/d6/5a59d938244a30735fe62d9433fd325f9021ea29d89780ea4596ea93bc89/jiter-0.16.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:8d031aabecc4f1b6276adfb42e3aabb77c89d468bf616600e8d3a11328929053", size = 350520, upload-time = "2026-06-29T13:03:46.671Z" }, + { url = "https://files.pythonhosted.org/packages/67/f8/c4a857f49c9af125f6bbcac7e3eee7f7978ed89682833062e2dbf62576b1/jiter-0.16.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:eab2cd170150e70153de16896a1774e3a1dca80154c56b54d7a812c479a7165e", size = 387550, upload-time = "2026-06-29T13:03:48.361Z" }, + { url = "https://files.pythonhosted.org/packages/8b/d6/5fbc2f7d6b67b754caa61a993a2e626e815dec47ffc2f9e35f01adfebec7/jiter-0.16.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:6edb63a46e65a82c26800a868e49b2cac30dd5a4218b88d74bc2c848c8ad60bb", size = 515424, upload-time = "2026-06-29T13:03:49.881Z" }, + { url = "https://files.pythonhosted.org/packages/ed/54/284f0164b64a5fed915fea6ba7e9ba9b3d8d37c67d59cf2e3bb99d45cdfe/jiter-0.16.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:659039cc50b5addcc35fcc87ae2c1833b7c0a8e5326ef631a75e4478447bcf84", size = 546981, upload-time = "2026-06-29T13:03:51.363Z" }, + { url = "https://files.pythonhosted.org/packages/13/c5/2a467585a576594384e1d2c43e1224deaafc085f24e243529cf98beef8e1/jiter-0.16.0-cp313-cp313-win32.whl", hash = "sha256:c9c53be232c2e206ef9cdbad81a48bfa74c3d3f08bcf8124630a8a748aad993e", size = 202853, upload-time = "2026-06-29T13:03:53.015Z" }, + { url = "https://files.pythonhosted.org/packages/88/6a/de61d04b9eec69c71719968d2f716532a3bc121170c44a39e14979c6be81/jiter-0.16.0-cp313-cp313-win_amd64.whl", hash = "sha256:baad945ed47f163ad833314f8e3288c396118934f94e7bbb9e243ce4b341a4fd", size = 196160, upload-time = "2026-06-29T13:03:54.447Z" }, + { url = "https://files.pythonhosted.org/packages/19/4b/b390ed59bafb3f31d008d1218578f10327714484b334439947f7e5b11e7f/jiter-0.16.0-cp313-cp313-win_arm64.whl", hash = "sha256:3c1fd2dbe1b0af19e987f03fe66c5f5bd105a2229c1aff4ab14890b24f41d21a", size = 189862, upload-time = "2026-06-29T13:03:55.754Z" }, + { url = "https://files.pythonhosted.org/packages/a7/89/bc4f1b57d5da938fd344a466396541e586d161320d70bffd929aaafcd8f4/jiter-0.16.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:b2c61484666ad42726029af0c00ef4541f0f3b5cdc550221f56c2343208018ee", size = 308239, upload-time = "2026-06-29T13:03:57.205Z" }, + { url = "https://files.pythonhosted.org/packages/65/7a/c415453e5213001bf3b411ff65dec3d303b0e76a4a2cfea9768cd4960994/jiter-0.16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:63efadc657488f45db1c676d81e704cac2abf3fdb892def1faea61db053127e2", size = 308928, upload-time = "2026-06-29T13:03:58.643Z" }, + { url = "https://files.pythonhosted.org/packages/11/fc/1f4fb7ebf9a724c7741994f4aae18fba1e2f3133df14521a79194952c34a/jiter-0.16.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf0d73f50e7b6935677854f6e8e31d499ca7064dd24734f703e060f5b237d883", size = 336998, upload-time = "2026-06-29T13:04:00.071Z" }, + { url = "https://files.pythonhosted.org/packages/a0/8d/72cadaac05ccfa7cc3a0a2232862e6c72443ca40cf300ba8b57f9f18b69b/jiter-0.16.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bf3ea07d9bc8e7d03a9fbc051295462e6dbc295b894fd72457c3136e3e43d898", size = 362112, upload-time = "2026-06-29T13:04:01.52Z" }, + { url = "https://files.pythonhosted.org/packages/58/4a/c4b0d5f651fda90a24ffce9f8d56cde462a2e09d31ae3de3c68cef34c04e/jiter-0.16.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:26798522707abb47d767db536e4148ceac1b14446bf028ee85e579a2e043cfe5", size = 459807, upload-time = "2026-06-29T13:04:03.214Z" }, + { url = "https://files.pythonhosted.org/packages/80/58/ef77879ea9aa56b50824edc5a445e226422c7a8d211f3fd2a56bcb9493cf/jiter-0.16.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bc837c1b9631be10abfe0191537fe8009838204cec7e44827401ace390ddb567", size = 373181, upload-time = "2026-06-29T13:04:04.629Z" }, + { url = "https://files.pythonhosted.org/packages/49/2e/ffbc3f254e4d8a66da3062c624a7df4b7c2b2cf9e1fe43cf394b3e104041/jiter-0.16.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49060fd70737fad59d33ba9dcc0d83247dc9e77187de26053a19c16c9f32bd69", size = 344927, upload-time = "2026-06-29T13:04:06.067Z" }, + { url = "https://files.pythonhosted.org/packages/9a/f6/0be5dc6d64a89f80aa8fec984f94dedb2973e251edcae55841d60786d578/jiter-0.16.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:adbb8edeadd431bc4477879d5d371ece7cb1334486584e0f252656dd7ffada29", size = 352754, upload-time = "2026-06-29T13:04:07.477Z" }, + { url = "https://files.pythonhosted.org/packages/da/6e/7d31243b3b91cd261dd19e9d3557fc3251a80883d3d8049c86174e7ab7af/jiter-0.16.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:31aaee5b80f672c1dc21272bcfb9cbdcfc1ea04ff50f00ed5af500b80c44fa93", size = 390553, upload-time = "2026-06-29T13:04:08.92Z" }, + { url = "https://files.pythonhosted.org/packages/25/33/51ae371fde3c88897520f62b4d5f8b27ad7103e2bb10812ff52195609853/jiter-0.16.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:6722bcef4ffc86c835574b1b2fac6b33b9fb4a889c781e67950e891591f3c55a", size = 516900, upload-time = "2026-06-29T13:04:10.407Z" }, + { url = "https://files.pythonhosted.org/packages/a0/45/6449b3d123ea439ba79507c657288f461d55049e7bcbdc2cf8eb8210f491/jiter-0.16.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:5ab4f50ff971b611d656554ea10b75f80097392c827bc32923c6eeb6386c8b00", size = 548754, upload-time = "2026-06-29T13:04:12.046Z" }, + { url = "https://files.pythonhosted.org/packages/9b/e7/fd2fb11ae3e2649333da3aa170d04d7b3000bbdc3b270f6513382fdf4e04/jiter-0.16.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:710cc51d4ebdcd3c1f70b232c1db1ea1344a075770422bbd4bede5708335acbe", size = 122381, upload-time = "2026-06-29T13:04:13.413Z" }, + { url = "https://files.pythonhosted.org/packages/26/80/f0b147a62c315a164ed2168908286ca302310824c218d3aae52b06c0c9a9/jiter-0.16.0-cp314-cp314-win32.whl", hash = "sha256:57b37fc887a32d44798e4d8ebfa7c9683ff3da1d5bf38f08d1bb3573ccb39106", size = 204578, upload-time = "2026-06-29T13:04:14.813Z" }, + { url = "https://files.pythonhosted.org/packages/5e/e6/4758a14304b4523a6f5adb2419340086aa3593bd4327c2b25b5948a90548/jiter-0.16.0-cp314-cp314-win_amd64.whl", hash = "sha256:cbd18dd5e2df96b580487b5745adf57ef64ad89ba2d9662fc3c19386acce7db8", size = 198154, upload-time = "2026-06-29T13:04:16.272Z" }, + { url = "https://files.pythonhosted.org/packages/26/be/41fa54a2e7ea41d6c99f1dc5b1f0fd4cb474680304b5d268dd518e81da3a/jiter-0.16.0-cp314-cp314-win_arm64.whl", hash = "sha256:a32d2027a9fa67f109ff245a3252ece3ccc32cc56703e1deab6cc846a59e0585", size = 191458, upload-time = "2026-06-29T13:04:17.707Z" }, + { url = "https://files.pythonhosted.org/packages/81/6b/59127338b86d9fe4d99418f5a15118bea778103ee0fe9d9dd7e0af174e95/jiter-0.16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2577196f4474ef3fc4779a088a23b0897bbf86f9ea3679c372d45b8383b43207", size = 316739, upload-time = "2026-06-29T13:04:19.663Z" }, + { url = "https://files.pythonhosted.org/packages/2d/95/49461034d5388196d3dabf98748935f017b7785d8f3f5349f834bcc4ed0d/jiter-0.16.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:616e89e008a93c01104161c75b4988e58716b01d62307ebfe161e52a56d2a818", size = 340911, upload-time = "2026-06-29T13:04:21.257Z" }, + { url = "https://files.pythonhosted.org/packages/cd/97/a4369f2fb82cb3dda13b98622f31249b2e014b223fe64ee534413ad72294/jiter-0.16.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0e2e9efbe042210df657bade597f66d6d75723e3d8f45a12ea6d8167ff8bbce3", size = 361747, upload-time = "2026-06-29T13:04:22.677Z" }, + { url = "https://files.pythonhosted.org/packages/28/51/49b6ed456261646e1906016a6760367a28aacd3c24805e4e5fe64116c1db/jiter-0.16.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f4d9e473a5ce7d27fef8b848df4dc16e283893d3f53b4a585e72c9595f3c284", size = 460225, upload-time = "2026-06-29T13:04:24.441Z" }, + { url = "https://files.pythonhosted.org/packages/33/b5/5689aff4f66c5b60be63106e591dbfcba2190df97d2c9c7cf052361ddb98/jiter-0.16.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d30a4a1c87713060c8d1cc59a7b6c8fb6b8ef0a6900368014c76c87922a2929", size = 373169, upload-time = "2026-06-29T13:04:25.884Z" }, + { url = "https://files.pythonhosted.org/packages/a2/96/3ae1b85ee0d6d6cab254fb7f8da018272b932bbf2d69b07e98aa2a96c746/jiter-0.16.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bae96332410f866e5900d809298b1ed82735932986c672495f9701daacd80620", size = 350332, upload-time = "2026-06-29T13:04:27.302Z" }, + { url = "https://files.pythonhosted.org/packages/15/32/c99d7bafd78986556c95bf60ce84c6cc98786eac56066c12d7f828bb6747/jiter-0.16.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:da3d7ec75dc83bb18bca888b5edfae0656a26849056c59e05a7728badd17e7af", size = 353377, upload-time = "2026-06-29T13:04:28.731Z" }, + { url = "https://files.pythonhosted.org/packages/0e/4b/f99a8e571287c3dec766bcc18528bbe8e8fb5365522ab5e6d64c93e87066/jiter-0.16.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ee6162b77d49a9939229df666dfa8af3e656b6701b54c4c84966d740e189264e", size = 387746, upload-time = "2026-06-29T13:04:30.319Z" }, + { url = "https://files.pythonhosted.org/packages/75/69/c78a5b3f71040e34eb5917df26fb7ae9a2174cad1ccbf277512507c53a6e/jiter-0.16.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:63ffdbdae7d4499f4cda14eadc12ddcabef0fc0c081191bdc2247489cb698077", size = 517292, upload-time = "2026-06-29T13:04:31.709Z" }, + { url = "https://files.pythonhosted.org/packages/c2/f7/095b38eda4c70d03651c403f29a5590f16d12ddc5d544aac9f9cddf72277/jiter-0.16.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:a111256a7193bea0759267b10385e5870949c239ed7b6ddbaaf57573edb38734", size = 549259, upload-time = "2026-06-29T13:04:33.721Z" }, + { url = "https://files.pythonhosted.org/packages/2e/c5/6a0207d90e5f656d95af98ebd0934f382d37674416f215aeda2ff8063e51/jiter-0.16.0-cp314-cp314t-win32.whl", hash = "sha256:de5ba8763e56b793561f43bed197c9ea55776daa5e9a6b91eed68a909bc9cdbf", size = 206523, upload-time = "2026-06-29T13:04:35.068Z" }, + { url = "https://files.pythonhosted.org/packages/a5/31/c757d5f30a8980fd945ce7b98be10be9e4ff59c7c42f5fd86804c2e87db8/jiter-0.16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b8a3f9a6008048fe9def7bf465180564a6e458047d2ce499149cfbe73c3ae9db", size = 200366, upload-time = "2026-06-29T13:04:36.61Z" }, + { url = "https://files.pythonhosted.org/packages/7c/a2/d88de6d313d734a544a7901353ad5db67cb38dcfcd91713b7979dafc345d/jiter-0.16.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0fa25b09b13075c46f5bc174f2690525a925a4fc2f7c82969a2bbabff22386ce", size = 190516, upload-time = "2026-06-29T13:04:38.004Z" }, + { url = "https://files.pythonhosted.org/packages/06/d3/8e278946d43eeca2585b4dd0834a887cd71136329b837f3a16ed86a8b4b0/jiter-0.16.0-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:850ccb1d7eedb4200f4014b1c0e8a577de114fc3cd88faad646dcc9bc4bb12ad", size = 304518, upload-time = "2026-06-29T13:05:00.172Z" }, + { url = "https://files.pythonhosted.org/packages/72/43/28d4ef495028bf0506a413d4db3f4eb3e7288a382e0f065f306a17bbeb5e/jiter-0.16.0-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:e34e97bda77eb63242a410243c071e28ac7e0d8c0948c5ee658498690a4b2f2f", size = 310207, upload-time = "2026-06-29T13:05:02.123Z" }, + { url = "https://files.pythonhosted.org/packages/e0/ca/c366b1012da1d640de975d9683acd44e4d150d9068845d0ca2610435253f/jiter-0.16.0-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b7dc85ea77d4abbae8bad0d3538678aedee75bceec4e2f6c8dfb1c74772e5aa5", size = 342771, upload-time = "2026-06-29T13:05:03.55Z" }, + { url = "https://files.pythonhosted.org/packages/16/52/50cc4056fc1ae02e7154704e7ecc89df0afb8300222cfe8a52d3f67e4730/jiter-0.16.0-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17ca7fae79f6d99cd9a042b75f917eaada7b895cfc7dd2ee3a16089dcaec7a85", size = 346468, upload-time = "2026-06-29T13:05:05.452Z" }, + { url = "https://files.pythonhosted.org/packages/98/ab/664fd8c4be028b2bedd3d2ff08769c4ede23d0dbc87a77c62384a0515b5d/jiter-0.16.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:f17d61a28b4b3e0e3e2ba98490c70501403b4d196f78732439160e7fd3678127", size = 303106, upload-time = "2026-06-29T13:05:07.118Z" }, + { url = "https://files.pythonhosted.org/packages/1a/07/421f1d5b65493a76e16027b848aba6a7d28073ae75944fa4289cc914d39f/jiter-0.16.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:96e38eea538c8ddf853a35727c7be0741c76c13f04148ac5c116222f50ece3b3", size = 304658, upload-time = "2026-06-29T13:05:08.708Z" }, + { url = "https://files.pythonhosted.org/packages/0a/db/bba1155f01a01c3c37a89425d571da751bbedf5c54247b831a04cb971798/jiter-0.16.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d284fb8d94d5855d60c44fefcab4bf966f1da6fada73992b01f6f0c9bc0c6702", size = 339719, upload-time = "2026-06-29T13:05:10.41Z" }, + { url = "https://files.pythonhosted.org/packages/78/f7/18a1afcd64f35314b68c1f23afcd9994d0bc13e65cc77517afff4e83986d/jiter-0.16.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64d613743df53199b1aa256a7d328340da6d7078aac7705a7db9d7a791e9cfd2", size = 343885, upload-time = "2026-06-29T13:05:12.087Z" }, ] [[package]] name = "joserfc" -version = "1.6.8" +version = "1.7.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cryptography" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5d/ac/d4fd5b30f82900eac60d765f179f0ba005825ac462cc8ced6e13ec685ab3/joserfc-1.6.8.tar.gz", hash = "sha256:878620c553a6ebdd76ccdc356782fee3f735f21a356d079a546b42a4670ace5f", size = 232930, upload-time = "2026-05-27T03:22:37.819Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/e0/27a6a081ae25420eda6768ceae05d7022a7f2447f420588843f2a44e4298/joserfc-1.7.4.tar.gz", hash = "sha256:b3bc561672ae541b17a9237053b48a03dacddd92d68047b3ecdfb4b5714a88ed", size = 234027, upload-time = "2026-07-19T15:43:02.739Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/98/8c/5cdce2cf3ce8155849baf9a5e2ce77e89dc87ec3bdb38259e5d85fbc45bd/joserfc-1.6.8-py3-none-any.whl", hash = "sha256:22fb31a69094a5e6f44632002a9df2c30c941fc6c8ce1b037e92c03de954cf9f", size = 70927, upload-time = "2026-05-27T03:22:35.796Z" }, + { url = "https://files.pythonhosted.org/packages/f9/bf/249dcd99b3376375910b7fa922383b57792975c8758f50d44612e749226c/joserfc-1.7.4-py3-none-any.whl", hash = "sha256:32d46c2cd5e3203c13e87a6c61333cab310b1ba80cd54b4c4f386a848a122463", size = 71000, upload-time = "2026-07-19T15:43:01.299Z" }, ] [[package]] @@ -1609,7 +1589,8 @@ dependencies = [ { name = "attrs" }, { name = "jsonschema-specifications" }, { name = "referencing" }, - { name = "rpds-py" }, + { name = "rpds-py", version = "0.30.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "rpds-py", version = "2026.6.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" } wheels = [ @@ -1618,16 +1599,17 @@ wheels = [ [[package]] name = "jsonschema-path" -version = "0.4.5" +version = "0.5.0" source = { registry = "https://pypi.org/simple" } dependencies = [ + { name = "attrs" }, { name = "pathable" }, { name = "pyyaml" }, { name = "referencing" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5b/8a/7e6102f2b8bdc6705a9eb5294f8f6f9ccd3a8420e8e8e19671d1dd773251/jsonschema_path-0.4.5.tar.gz", hash = "sha256:c6cd7d577ae290c7defd4f4029e86fdb248ca1bd41a07557795b3c95e5144918", size = 15113, upload-time = "2026-03-03T09:56:46.87Z" } +sdist = { url = "https://files.pythonhosted.org/packages/39/79/cd02a4df6d9270efdc7d3feefe6edd730b0820c39eeaa107a2faee8322d5/jsonschema_path-0.5.0.tar.gz", hash = "sha256:493b156ba895c97602655b620a8456caa2ce08c1aa389f5a7addec065e6e855c", size = 19597, upload-time = "2026-05-19T20:45:00.971Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/04/d5/4e96c44f6c1ea3d812cf5391d81a4f5abaa540abf8d04ecd7f66e0ed11df/jsonschema_path-0.4.5-py3-none-any.whl", hash = "sha256:7d77a2c3f3ec569a40efe5c5f942c44c1af2a6f96fe0866794c9ef5b8f87fd65", size = 19368, upload-time = "2026-03-03T09:56:45.39Z" }, + { url = "https://files.pythonhosted.org/packages/04/2c/9e69d73c4297508be9e3b64a970ea3971b3eb8db64ffc5802d40bd25981f/jsonschema_path-0.5.0-py3-none-any.whl", hash = "sha256:2790a070bc7abb08ea3dbe4d340ece4efadf639223001f020c7503229ba068e2", size = 24077, upload-time = "2026-05-19T20:44:59.225Z" }, ] [[package]] @@ -1677,31 +1659,31 @@ wheels = [ [[package]] name = "markdown-it-py" -version = "4.0.0" +version = "4.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mdurl" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } +sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, + { url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" }, ] [[package]] name = "matplotlib-inline" -version = "0.2.1" +version = "0.2.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "traitlets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c7/74/97e72a36efd4ae2bccb3463284300f8953f199b5ffbc04cbbb0ec78f74b1/matplotlib_inline-0.2.1.tar.gz", hash = "sha256:e1ee949c340d771fc39e241ea75683deb94762c8fa5f2927ec57c83c4dffa9fe", size = 8110, upload-time = "2025-10-23T09:00:22.126Z" } +sdist = { url = "https://files.pythonhosted.org/packages/bd/c0/9f7c9a46090390368a4d7bcb76bb87a4a36c421e4c0792cdb53486ffac7a/matplotlib_inline-0.2.2.tar.gz", hash = "sha256:72f3fe8fce36b70d4a5b612f899090cd0401deddc4ea90e1572b9f4bfb058c79", size = 8150, upload-time = "2026-05-08T17:33:33.49Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/af/33/ee4519fa02ed11a94aef9559552f3b17bb863f2ecfe1a35dc7f548cde231/matplotlib_inline-0.2.1-py3-none-any.whl", hash = "sha256:d56ce5156ba6085e00a9d54fead6ed29a9c47e215cd1bba2e976ef39f5710a76", size = 9516, upload-time = "2025-10-23T09:00:20.675Z" }, + { url = "https://files.pythonhosted.org/packages/41/09/5b161152e2d90f7b87f781c2e1267494aef9c32498df793f73ad0a0a494a/matplotlib_inline-0.2.2-py3-none-any.whl", hash = "sha256:3c821cf1c209f59fb2d2d64abbf5b23b67bcb2210d663f9918dd851c6da1fcf6", size = 9534, upload-time = "2026-05-08T17:33:32.055Z" }, ] [[package]] name = "mcp" -version = "2.0.0b2" +version = "2.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -1710,7 +1692,6 @@ dependencies = [ { name = "mcp-types" }, { name = "opentelemetry-api" }, { name = "pydantic" }, - { name = "pydantic-settings" }, { name = "pyjwt", extra = ["crypto"] }, { name = "python-multipart" }, { name = "pywin32", marker = "sys_platform == 'win32'" }, @@ -1720,22 +1701,22 @@ dependencies = [ { name = "typing-inspection" }, { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a1/aa/c5d38e0199304494be6370667d04d93d8681d9bdc864d56678250dbd3f3b/mcp-2.0.0b2.tar.gz", hash = "sha256:0528d0d38ae798fbff251616ec687faaaa8f5309571e0b2bc553c530fa10b8b1", size = 1590650, upload-time = "2026-07-14T16:47:57.897Z" } +sdist = { url = "https://files.pythonhosted.org/packages/74/33/32d4dff2c95bb5d897c3ef4c83649a08996b17b58f0a326d2495d4c81179/mcp-2.0.0.tar.gz", hash = "sha256:0f440e735c13ece8bb19bc62cf0b86f4313448432fbb77d35e14034f4e050728", size = 1662284, upload-time = "2026-07-28T13:45:32.346Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a7/90/187d6283a304acc6954987992ef17e2515739a649f148dc8b67b302e1bbd/mcp-2.0.0b2-py3-none-any.whl", hash = "sha256:9c50ae5afa08960ab76d50aa3adab3184952d9bea7ef87f4a4a5ba68bdefcf0a", size = 334286, upload-time = "2026-07-14T16:47:54.768Z" }, + { url = "https://files.pythonhosted.org/packages/67/72/7d7897418912c1d12e87556630dfb7bf0eac71160e9bef8b447960804ee3/mcp-2.0.0-py3-none-any.whl", hash = "sha256:1cb4c75d2d2c7b8c1d756355e5d82a39f2822cc7f13e22a2051d7ca3592349d6", size = 349980, upload-time = "2026-07-28T13:45:28.853Z" }, ] [[package]] name = "mcp-types" -version = "2.0.0b2" +version = "2.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pydantic" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ad/21/db529130ac8edd1d844fc322862afeceaf2b7f610a591fa528002b022e07/mcp_types-2.0.0b2.tar.gz", hash = "sha256:094fa7160106819ab39a1586179c3a9f070bfd833d0a6f8fcb30a54a986cc402", size = 65877, upload-time = "2026-07-14T16:47:59.198Z" } +sdist = { url = "https://files.pythonhosted.org/packages/bb/56/9b8e1c152f61f6c6b07c4b5896c88c7d0ae90bac6ee6306f852fcc5c1eb0/mcp_types-2.0.0.tar.gz", hash = "sha256:d7d939b9285c9961ae8866ba75ef85da34d12bafe276efbf4eb6a131786d8379", size = 66632, upload-time = "2026-07-28T13:45:33.804Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/e1/c466ceacdaa929396d35ad45176398acd0c416fead0970d3ad22618a19d7/mcp_types-2.0.0b2-py3-none-any.whl", hash = "sha256:35c9c33abb90a77dc6ad1daecaa6407c788c2f32d14d52dad8f843c4f008eae2", size = 68944, upload-time = "2026-07-14T16:47:56.493Z" }, + { url = "https://files.pythonhosted.org/packages/f5/4c/c78d78c3d52b0ac594ad7cc8ef5972adfe070e3597a8a4c6ce0cd39196ea/mcp_types-2.0.0-py3-none-any.whl", hash = "sha256:6b2de797ca2797f568b79529e1b25948e34de511bcc0bd82fef1039a6d1b8eb0", size = 69649, upload-time = "2026-07-28T13:45:30.713Z" }, ] [[package]] @@ -1749,11 +1730,11 @@ wheels = [ [[package]] name = "more-itertools" -version = "10.8.0" +version = "11.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ea/5d/38b681d3fce7a266dd9ab73c66959406d565b3e85f21d5e66e1181d93721/more_itertools-10.8.0.tar.gz", hash = "sha256:f638ddf8a1a0d134181275fb5d58b086ead7c6a72429ad725c67503f13ba30bd", size = 137431, upload-time = "2025-09-02T15:23:11.018Z" } +sdist = { url = "https://files.pythonhosted.org/packages/de/1d/f4da6f02cdffe04d6362210b807146a26044c88d839208aec273bb0d9184/more_itertools-11.1.0.tar.gz", hash = "sha256:48e8f4d9e7e5878571ecf6f2b4e57634f93cd474cc8cfbd2376f2d11b396e30d", size = 145772, upload-time = "2026-05-22T14:14:29.909Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a4/8e/469e5a4a2f5855992e425f3cb33804cc07bf18d48f2db061aec61ce50270/more_itertools-10.8.0-py3-none-any.whl", hash = "sha256:52d4362373dcf7c52546bc4af9a86ee7c4579df9a8dc268be0a2f949d376cc9b", size = 69667, upload-time = "2025-09-02T15:23:09.635Z" }, + { url = "https://files.pythonhosted.org/packages/e8/3d/1087453384dbde46a8c7f9356eead2c58be8a7bf156bca40243377c85715/more_itertools-11.1.0-py3-none-any.whl", hash = "sha256:4b65538ae22f6fed0ce4874efd317463a7489796a0939fa66824dd542125a192", size = 72226, upload-time = "2026-05-22T14:14:28.824Z" }, ] [[package]] @@ -1793,7 +1774,7 @@ wheels = [ [[package]] name = "openai" -version = "2.30.0" +version = "2.46.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -1805,9 +1786,9 @@ dependencies = [ { name = "tqdm" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/88/15/52580c8fbc16d0675d516e8749806eda679b16de1e4434ea06fb6feaa610/openai-2.30.0.tar.gz", hash = "sha256:92f7661c990bda4b22a941806c83eabe4896c3094465030dd882a71abe80c885", size = 676084, upload-time = "2026-03-25T22:08:59.96Z" } +sdist = { url = "https://files.pythonhosted.org/packages/af/ac/f725c4efbda8657d02be684607e5a2e5ce362e4790fdbcbdfb7c15018647/openai-2.46.0.tar.gz", hash = "sha256:0421e0735ac41451cad894af4cddf0435bfbf8cbc538ac0e15b3c062f2ddc06a", size = 1114628, upload-time = "2026-07-17T02:48:06.05Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/9e/5bfa2270f902d5b92ab7d41ce0475b8630572e71e349b2a4996d14bdda93/openai-2.30.0-py3-none-any.whl", hash = "sha256:9a5ae616888eb2748ec5e0c5b955a51592e0b201a11f4262db920f2a78c5231d", size = 1146656, upload-time = "2026-03-25T22:08:58.2Z" }, + { url = "https://files.pythonhosted.org/packages/ea/7b/206238ebcb50b235942b1c66dba4974776f2057402a8d91c399be587d66a/openai-2.46.0-py3-none-any.whl", hash = "sha256:672381db55efb3a1e2610f29304c130cccdd0b319bace4d492b2443cb64c1e7c", size = 1637556, upload-time = "2026-07-17T02:48:03.695Z" }, ] [[package]] @@ -1824,32 +1805,31 @@ wheels = [ [[package]] name = "opentelemetry-api" -version = "1.40.0" +version = "1.44.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "importlib-metadata" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2c/1d/4049a9e8698361cc1a1aa03a6c59e4fa4c71e0c0f94a30f988a6876a2ae6/opentelemetry_api-1.40.0.tar.gz", hash = "sha256:159be641c0b04d11e9ecd576906462773eb97ae1b657730f0ecf64d32071569f", size = 70851, upload-time = "2026-03-04T14:17:21.555Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/8b/aa9e2d8b8dfa7c946f7dec5d1f8f6ba8eca062f43509a06bdb5ce93d26c0/opentelemetry_api-1.44.0.tar.gz", hash = "sha256:67647e5e9566edcf421166fdf022b3537f818635daa852b289e34604dc6fb33a", size = 72406, upload-time = "2026-07-16T15:25:32.678Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5f/bf/93795954016c522008da367da292adceed71cca6ee1717e1d64c83089099/opentelemetry_api-1.40.0-py3-none-any.whl", hash = "sha256:82dd69331ae74b06f6a874704be0cfaa49a1650e1537d4a813b86ecef7d0ecf9", size = 68676, upload-time = "2026-03-04T14:17:01.24Z" }, + { url = "https://files.pythonhosted.org/packages/ca/6f/a04e900f465ff3221ccc395522503e2d10e79fa21f2723c8e177aae1e0d1/opentelemetry_api-1.44.0-py3-none-any.whl", hash = "sha256:94b98c893a91b88657eaac1e3ba89618cdb85be6918196705354f34728b2cdef", size = 60018, upload-time = "2026-07-16T15:25:11.657Z" }, ] [[package]] name = "opentelemetry-exporter-otlp-proto-common" -version = "1.40.0" +version = "1.44.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-proto" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/51/bc/1559d46557fe6eca0b46c88d4c2676285f1f3be2e8d06bb5d15fbffc814a/opentelemetry_exporter_otlp_proto_common-1.40.0.tar.gz", hash = "sha256:1cbee86a4064790b362a86601ee7934f368b81cd4cc2f2e163902a6e7818a0fa", size = 20416, upload-time = "2026-03-04T14:17:23.801Z" } +sdist = { url = "https://files.pythonhosted.org/packages/61/09/4d717852c1cf3f854b76c7110a5d00883bc3c99288b9b0dbcbeb9e306eb6/opentelemetry_exporter_otlp_proto_common-1.44.0.tar.gz", hash = "sha256:dc87a5a5bc58f149a56d1547e4691588fa12994cdc3bc039a694ccb3375862ac", size = 20202, upload-time = "2026-07-16T15:25:37.658Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8b/ca/8f122055c97a932311a3f640273f084e738008933503d0c2563cd5d591fc/opentelemetry_exporter_otlp_proto_common-1.40.0-py3-none-any.whl", hash = "sha256:7081ff453835a82417bf38dccf122c827c3cbc94f2079b03bba02a3165f25149", size = 18369, upload-time = "2026-03-04T14:17:04.796Z" }, + { url = "https://files.pythonhosted.org/packages/5e/71/65fd9d54c10b860f87c045ccee1264cab7011268895d3528818a29c1172a/opentelemetry_exporter_otlp_proto_common-1.44.0-py3-none-any.whl", hash = "sha256:9a9fe61bba73d802904bc989f1d6b4a7b1ee40f06c40e98d6f85af65aaebb694", size = 17045, upload-time = "2026-07-16T15:25:18.201Z" }, ] [[package]] name = "opentelemetry-exporter-otlp-proto-grpc" -version = "1.40.0" +version = "1.44.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "googleapis-common-protos" }, @@ -1860,84 +1840,84 @@ dependencies = [ { name = "opentelemetry-sdk" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/8f/7f/b9e60435cfcc7590fa87436edad6822240dddbc184643a2a005301cc31f4/opentelemetry_exporter_otlp_proto_grpc-1.40.0.tar.gz", hash = "sha256:bd4015183e40b635b3dab8da528b27161ba83bf4ef545776b196f0fb4ec47740", size = 25759, upload-time = "2026-03-04T14:17:24.4Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1f/47/80d9e9d468dc5de3af5096f5ccdb065fa4dd1470f74495cc53e59e397f47/opentelemetry_exporter_otlp_proto_grpc-1.44.0.tar.gz", hash = "sha256:40d1ae9e03fcc36de3cbac610cc99f35894938bff9cfd90fc4ec68bd85448463", size = 27225, upload-time = "2026-07-16T15:25:38.308Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/96/6f/7ee0980afcbdcd2d40362da16f7f9796bd083bf7f0b8e038abfbc0300f5d/opentelemetry_exporter_otlp_proto_grpc-1.40.0-py3-none-any.whl", hash = "sha256:2aa0ca53483fe0cf6405087a7491472b70335bc5c7944378a0a8e72e86995c52", size = 20304, upload-time = "2026-03-04T14:17:05.942Z" }, + { url = "https://files.pythonhosted.org/packages/54/29/6ae42ba32b153ae0a44ae125f0caff2188bbe62d99c82d1768da30864e72/opentelemetry_exporter_otlp_proto_grpc-1.44.0-py3-none-any.whl", hash = "sha256:6a1a645ea182a2f59440c51fa8301d309f3324a8f9d65f8395584b064b67ee4e", size = 19624, upload-time = "2026-07-16T15:25:19.096Z" }, ] [[package]] name = "opentelemetry-proto" -version = "1.40.0" +version = "1.44.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "protobuf" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/4c/77/dd38991db037fdfce45849491cb61de5ab000f49824a00230afb112a4392/opentelemetry_proto-1.40.0.tar.gz", hash = "sha256:03f639ca129ba513f5819810f5b1f42bcb371391405d99c168fe6937c62febcd", size = 45667, upload-time = "2026-03-04T14:17:31.194Z" } +sdist = { url = "https://files.pythonhosted.org/packages/64/01/40ac4ae9a149263cc52c2cee200ddd80cb6d8db1a4610abf8eabce0fe771/opentelemetry_proto-1.44.0.tar.gz", hash = "sha256:c547a79c2f8c0c515d31509154682e5921c7cfd5ca67b70e1f9266e2c3e103f3", size = 46488, upload-time = "2026-07-16T15:25:45.34Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b9/b2/189b2577dde745b15625b3214302605b1353436219d42b7912e77fa8dc24/opentelemetry_proto-1.40.0-py3-none-any.whl", hash = "sha256:266c4385d88923a23d63e353e9761af0f47a6ed0d486979777fe4de59dc9b25f", size = 72073, upload-time = "2026-03-04T14:17:16.673Z" }, + { url = "https://files.pythonhosted.org/packages/d1/7c/8be563d68e93bbefa5c8affb82ddcff91b3ad858ce49957ba7b16fd3e0ab/opentelemetry_proto-1.44.0-py3-none-any.whl", hash = "sha256:898b155a0e1557afd867478fb6158e8122a46329ca0bb8dc53cc55e98f017f56", size = 72483, upload-time = "2026-07-16T15:25:28.429Z" }, ] [[package]] name = "opentelemetry-sdk" -version = "1.40.0" +version = "1.44.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-semantic-conventions" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/58/fd/3c3125b20ba18ce2155ba9ea74acb0ae5d25f8cd39cfd37455601b7955cc/opentelemetry_sdk-1.40.0.tar.gz", hash = "sha256:18e9f5ec20d859d268c7cb3c5198c8d105d073714db3de50b593b8c1345a48f2", size = 184252, upload-time = "2026-03-04T14:17:31.87Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5d/77/a6592cbc7c8d9bcc9d6757a9df45e04a7c585e3e6e7a13456da522b21109/opentelemetry_sdk-1.44.0.tar.gz", hash = "sha256:cebe7f65dc12f26ead75c6064de12fd2a9052e5060c0272d402cfa203aae123b", size = 208624, upload-time = "2026-07-16T15:25:46.078Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/c5/6a852903d8bfac758c6dc6e9a68b015d3c33f2f1be5e9591e0f4b69c7e0a/opentelemetry_sdk-1.40.0-py3-none-any.whl", hash = "sha256:787d2154a71f4b3d81f20524a8ce061b7db667d24e46753f32a7bc48f1c1f3f1", size = 141951, upload-time = "2026-03-04T14:17:17.961Z" }, + { url = "https://files.pythonhosted.org/packages/e7/23/ff077e61886ee020a17ce9c8b6fa11c601c8d8345b09ea24f605445df62a/opentelemetry_sdk-1.44.0-py3-none-any.whl", hash = "sha256:df081c4c6bcfdb1211e3e86140376792643128a25f8d72d1d27675936e7e96ad", size = 137221, upload-time = "2026-07-16T15:25:29.534Z" }, ] [[package]] name = "opentelemetry-semantic-conventions" -version = "0.61b0" +version = "0.65b0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/6d/c0/4ae7973f3c2cfd2b6e321f1675626f0dab0a97027cc7a297474c9c8f3d04/opentelemetry_semantic_conventions-0.61b0.tar.gz", hash = "sha256:072f65473c5d7c6dc0355b27d6c9d1a679d63b6d4b4b16a9773062cb7e31192a", size = 145755, upload-time = "2026-03-04T14:17:32.664Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8f/73/0cbdebcb4cf545fdd328da14f5137e37d0770c3f26185e478b0d15d94f50/opentelemetry_semantic_conventions-0.65b0.tar.gz", hash = "sha256:f9b2b81e9d5b64f11bc952075e7e9c7fb0aab075c7fd1c46d597f1b919852d60", size = 148774, upload-time = "2026-07-16T15:25:46.902Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b2/37/cc6a55e448deaa9b27377d087da8615a3416d8ad523d5960b78dbeadd02a/opentelemetry_semantic_conventions-0.61b0-py3-none-any.whl", hash = "sha256:fa530a96be229795f8cef353739b618148b0fe2b4b3f005e60e262926c4d38e2", size = 231621, upload-time = "2026-03-04T14:17:19.33Z" }, + { url = "https://files.pythonhosted.org/packages/a6/0e/49df70d9b81fb5cbae4bbf2a49d865b09bcbcbc4eb53f5851b1027738d78/opentelemetry_semantic_conventions-0.65b0-py3-none-any.whl", hash = "sha256:1cacde7b0ad306f84c5ef08c3dbe1bbaf20165bba6f8bff43b670e555a086bcb", size = 204645, upload-time = "2026-07-16T15:25:30.688Z" }, ] [[package]] name = "packaging" -version = "26.0" +version = "26.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, + { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, ] [[package]] name = "parso" -version = "0.8.6" +version = "0.8.7" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/81/76/a1e769043c0c0c9fe391b702539d594731a4362334cdf4dc25d0c09761e7/parso-0.8.6.tar.gz", hash = "sha256:2b9a0332696df97d454fa67b81618fd69c35a7b90327cbe6ba5c92d2c68a7bfd", size = 401621, upload-time = "2026-02-09T15:45:24.425Z" } +sdist = { url = "https://files.pythonhosted.org/packages/30/4b/90c937815137d43ce71ba043cd3566221e9df6b9c805f24b5d138c9d40a7/parso-0.8.7.tar.gz", hash = "sha256:eaaac4c9fdd5e9e8852dc778d2d7405897ec510f2a298071453e5e3a07914bb1", size = 401824, upload-time = "2026-05-01T23:13:02.138Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b6/61/fae042894f4296ec49e3f193aff5d7c18440da9e48102c3315e1bc4519a7/parso-0.8.6-py2.py3-none-any.whl", hash = "sha256:2c549f800b70a5c4952197248825584cb00f033b29c692671d3bf08bf380baff", size = 106894, upload-time = "2026-02-09T15:45:21.391Z" }, + { url = "https://files.pythonhosted.org/packages/99/5d/8268b644392ee874ee82a635cd0df1773de230bde356c38de28e298392cc/parso-0.8.7-py2.py3-none-any.whl", hash = "sha256:a8926eb2a1b915486941fdbd31e86a4baf88fe8c210f25f2f35ecec5b574ca1c", size = 107025, upload-time = "2026-05-01T23:12:58.867Z" }, ] [[package]] name = "pathable" -version = "0.5.0" +version = "0.6.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/72/55/b748445cb4ea6b125626f15379be7c96d1035d4fa3e8fee362fa92298abf/pathable-0.5.0.tar.gz", hash = "sha256:d81938348a1cacb525e7c75166270644782c0fb9c8cecc16be033e71427e0ef1", size = 16655, upload-time = "2026-02-20T08:47:00.748Z" } +sdist = { url = "https://files.pythonhosted.org/packages/66/f3/5a20387de9bcd0607871bfc2198ee0e15836da7baa4592ccd7f24c27c986/pathable-0.6.0.tar.gz", hash = "sha256:6404b8b82aef5ff0fd478934137128b99b12212ba35afdde5525ca4f8388ea58", size = 18970, upload-time = "2026-05-19T18:15:11.911Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/52/96/5a770e5c461462575474468e5af931cff9de036e7c2b4fea23c1c58d2cbe/pathable-0.5.0-py3-none-any.whl", hash = "sha256:646e3d09491a6351a0c82632a09c02cdf70a252e73196b36d8a15ba0a114f0a6", size = 16867, upload-time = "2026-02-20T08:46:59.536Z" }, + { url = "https://files.pythonhosted.org/packages/a2/e8/6d75ffd9784bce2e93d1ae4415649427e39a53bb172d4672b2b59c6f0a7b/pathable-0.6.0-py3-none-any.whl", hash = "sha256:82c4ca6c98c502ad12e0d4e9779b6210afee93c38990988c8c5d1b49bdcdf566", size = 18983, upload-time = "2026-05-19T18:15:10.728Z" }, ] [[package]] name = "pathspec" -version = "1.0.4" +version = "1.1.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fa/36/e27608899f9b8d4dff0617b2d9ab17ca5608956ca44461ac14ac48b44015/pathspec-1.0.4.tar.gz", hash = "sha256:0210e2ae8a21a9137c0d470578cb0e595af87edaa6ebf12ff176f14a02e0e645", size = 131200, upload-time = "2026-01-27T03:59:46.938Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/82/42f767fc1c1143d6fd36efb827202a2d997a375e160a71eb2888a925aac1/pathspec-1.1.1.tar.gz", hash = "sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a", size = 135180, upload-time = "2026-04-27T01:46:08.907Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/3c/2c197d226f9ea224a9ab8d197933f9da0ae0aac5b6e0f884e2b8d9c8e9f7/pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723", size = 55206, upload-time = "2026-01-27T03:59:45.137Z" }, + { url = "https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189", size = 57328, upload-time = "2026-04-27T01:46:07.06Z" }, ] [[package]] @@ -1967,11 +1947,11 @@ wheels = [ [[package]] name = "platformdirs" -version = "4.9.4" +version = "4.11.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/19/56/8d4c30c8a1d07013911a8fdbd8f89440ef9f08d07a1b50ab8ca8be5a20f9/platformdirs-4.9.4.tar.gz", hash = "sha256:1ec356301b7dc906d83f371c8f487070e99d3ccf9e501686456394622a01a934", size = 28737, upload-time = "2026-03-05T18:34:13.271Z" } +sdist = { url = "https://files.pythonhosted.org/packages/78/9b/560e4be8e26f6fd133a03630a8df0c663b9e8d61b4ade152b72005aec83b/platformdirs-4.11.0.tar.gz", hash = "sha256:0555d18370482847566ffabcaa53ad7c6c1c29f195989ae1ed634a05f76ea1e0", size = 31953, upload-time = "2026-07-21T13:09:36.565Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/63/d7/97f7e3a6abb67d8080dd406fd4df842c2be0efaf712d1c899c32a075027c/platformdirs-4.9.4-py3-none-any.whl", hash = "sha256:68a9a4619a666ea6439f2ff250c12a853cd1cbd5158d258bd824a7df6be2f868", size = 21216, upload-time = "2026-03-05T18:34:12.172Z" }, + { url = "https://files.pythonhosted.org/packages/7d/68/d8d58938dfb1370b266a1a729e6d77a985be23689a0496498ee17b2cbf90/platformdirs-4.11.0-py3-none-any.whl", hash = "sha256:360ccded2b7fce0af0ff80cc8f5942a1c5d99b0e856033acb030bfc634709e74", size = 23247, upload-time = "2026-07-21T13:09:35.422Z" }, ] [[package]] @@ -1985,49 +1965,49 @@ wheels = [ [[package]] name = "prefab-ui" -version = "0.18.0" +version = "0.20.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cyclopts" }, { name = "pydantic" }, { name = "rich" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2f/a3/25fe72b9887d9c2daa0ec5e79a7971a67aad31a6f71d634e23da662343ad/prefab_ui-0.18.0.tar.gz", hash = "sha256:f72e241f52f4720baac670f8527c773e1c1f4b558bce4f77097441eecbb51b9e", size = 3998186, upload-time = "2026-03-30T01:13:33.419Z" } +sdist = { url = "https://files.pythonhosted.org/packages/41/99/4e61eb3d3f8b09bdaa28bda3c99971264555f486a485333cb8e6f56c7d8c/prefab_ui-0.20.2.tar.gz", hash = "sha256:4ac17ebf8ec1c5a918a188625837fc608157907430a59c4feb8adac80e01262b", size = 4118979, upload-time = "2026-06-03T02:13:49.312Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c0/dd/28be02a264c59d64086122c8b0f9fa99fc52e040682358e5e08219846961/prefab_ui-0.18.0-py3-none-any.whl", hash = "sha256:c9d01bd423b0d5bf103d9a0e6cfac135bd973d416297c32a5bbccc182161cace", size = 1824803, upload-time = "2026-03-30T01:13:31.243Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d0/59b697dd5a44e632fcc4aba42ff5f88224df672ffea1f5e9a5a9e9e50698/prefab_ui-0.20.2-py3-none-any.whl", hash = "sha256:861d4914e4d9120996b4d5c6753788beeca433754d7bb3cfbd13f9dba7ea8e85", size = 1852274, upload-time = "2026-06-03T02:13:47.539Z" }, ] [[package]] name = "prek" -version = "0.3.8" +version = "0.4.10" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/62/ee/03e8180e3fda9de25b6480bd15cc2bde40d573868d50648b0e527b35562f/prek-0.3.8.tar.gz", hash = "sha256:434a214256516f187a3ab15f869d950243be66b94ad47987ee4281b69643a2d9", size = 400224, upload-time = "2026-03-23T08:23:35.981Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7c/54/edc21e275f9fa3540d4d98cf349c2de11621d6729cc401bb7aedf563609e/prek-0.4.10.tar.gz", hash = "sha256:db3122f4e780eb4587635e6a83df881caf2dbb1eb7799d1cca51158216d6f33b", size = 502565, upload-time = "2026-07-16T10:13:00.788Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/00/84/40d2ddf362d12c4cd4a25a8c89a862edf87cdfbf1422aa41aac8e315d409/prek-0.3.8-py3-none-linux_armv6l.whl", hash = "sha256:6fb646ada60658fa6dd7771b2e0fb097f005151be222f869dada3eb26d79ed33", size = 5226646, upload-time = "2026-03-23T08:23:18.306Z" }, - { url = "https://files.pythonhosted.org/packages/e1/52/7308a033fa43b7e8e188797bd2b3b017c0f0adda70fa7af575b1f43ea888/prek-0.3.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:f3d7fdadb15efc19c09953c7a33cf2061a70f367d1e1957358d3ad5cc49d0616", size = 5620104, upload-time = "2026-03-23T08:23:40.053Z" }, - { url = "https://files.pythonhosted.org/packages/ff/b1/f106ac000a91511a9cd80169868daf2f5b693480ef5232cec5517a38a512/prek-0.3.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:72728c3295e79ca443f8c1ec037d2a5b914ec73a358f69cf1bc1964511876bf8", size = 5199867, upload-time = "2026-03-23T08:23:38.066Z" }, - { url = "https://files.pythonhosted.org/packages/b3/e9/970713f4b019f69de9844e1bab37b8ddb67558e410916f4eb5869a696165/prek-0.3.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:48efc28f2f53b5b8087efca9daaed91572d62df97d5f24a1c7a087fecb5017de", size = 5441801, upload-time = "2026-03-23T08:23:32.617Z" }, - { url = "https://files.pythonhosted.org/packages/12/a4/7ef44032b181753e19452ec3b09abb3a32607cf6b0a0508f0604becaaf2b/prek-0.3.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f6ca9d63bacbc448a5c18e955c78d3ac5176c3a17c3baacdd949b1a623e08a36", size = 5155107, upload-time = "2026-03-23T08:23:31.021Z" }, - { url = "https://files.pythonhosted.org/packages/bd/77/4d9c8985dbba84149760785dfe07093ea1e29d710257dfb7c89615e2234c/prek-0.3.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1000f7029696b4fe712fb1fefd4c55b9c4de72b65509c8e50296370a06f9dc3f", size = 5566541, upload-time = "2026-03-23T08:23:45.694Z" }, - { url = "https://files.pythonhosted.org/packages/1a/1a/81e6769ac1f7f8346d09ce2ab0b47cf06466acd9ff72e87e5d1f0d98cd32/prek-0.3.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6ff0bed0e2c1286522987d982168a86cbbd0d069d840506a46c9fda983515517", size = 6552991, upload-time = "2026-03-23T08:23:21.958Z" }, - { url = "https://files.pythonhosted.org/packages/6f/fa/ce2df0dd2dc75a9437a52463239d0782998943d7b04e191fb89b83016c34/prek-0.3.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4fb087ac0ffda3ac65bbbae9a38326a7fd27ee007bb4a94323ce1eb539d8bbec", size = 5832972, upload-time = "2026-03-23T08:23:20.258Z" }, - { url = "https://files.pythonhosted.org/packages/18/6b/9d4269df9073216d296244595a21c253b6475dfc9076c0bd2906be7a436c/prek-0.3.8-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:2e1e5e206ff7b31bd079cce525daddc96cd6bc544d20dc128921ad92f7a4c85d", size = 5448371, upload-time = "2026-03-23T08:23:41.835Z" }, - { url = "https://files.pythonhosted.org/packages/60/1d/1e4d8a78abefa5b9d086e5a9f1638a74b5e540eec8a648d9946707701f29/prek-0.3.8-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:dcea3fe23832a4481bccb7c45f55650cb233be7c805602e788bb7dba60f2d861", size = 5270546, upload-time = "2026-03-23T08:23:24.231Z" }, - { url = "https://files.pythonhosted.org/packages/77/07/34f36551a6319ae36e272bea63a42f59d41d2d47ab0d5fb00eb7b4e88e87/prek-0.3.8-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:4d25e647e9682f6818ab5c31e7a4b842993c14782a6ffcd128d22b784e0d677f", size = 5124032, upload-time = "2026-03-23T08:23:26.368Z" }, - { url = "https://files.pythonhosted.org/packages/e3/01/6d544009bb655e709993411796af77339f439526db4f3b3509c583ad8eb9/prek-0.3.8-py3-none-musllinux_1_1_i686.whl", hash = "sha256:de528b82935e33074815acff3c7c86026754d1212136295bc88fe9c43b4231d5", size = 5432245, upload-time = "2026-03-23T08:23:47.877Z" }, - { url = "https://files.pythonhosted.org/packages/54/96/1237ee269e9bfa283ffadbcba1f401f48a47aed2b2563eb1002740d6079d/prek-0.3.8-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:6d660f1c25a126e6d9f682fe61449441226514f412a4469f5d71f8f8cad56db2", size = 5950550, upload-time = "2026-03-23T08:23:43.8Z" }, - { url = "https://files.pythonhosted.org/packages/ca/6b/a574411459049bc691047c9912f375deda10c44a707b6ce98df2b658f0b3/prek-0.3.8-py3-none-win32.whl", hash = "sha256:b0c291c577615d9f8450421dff0b32bfd77a6b0d223ee4115a1f820cb636fdf1", size = 4949501, upload-time = "2026-03-23T08:23:16.338Z" }, - { url = "https://files.pythonhosted.org/packages/0c/b4/46b59fe49f635acd9f6530778ce577f9d8b49452835726a5311ffc902c67/prek-0.3.8-py3-none-win_amd64.whl", hash = "sha256:bc147fdbdd4ec33fc7a987b893ecb69b1413ac100d95c9889a70f3fd58c73d06", size = 5346551, upload-time = "2026-03-23T08:23:34.501Z" }, - { url = "https://files.pythonhosted.org/packages/53/05/9cca1708bb8c65264124eb4b04251e0f65ce5bfc707080bb6b492d5a0df7/prek-0.3.8-py3-none-win_arm64.whl", hash = "sha256:a2614647aeafa817a5802ccb9561e92eedc20dcf840639a1b00826e2c2442515", size = 5190872, upload-time = "2026-03-23T08:23:29.463Z" }, + { url = "https://files.pythonhosted.org/packages/db/e7/5a63528ba7b95b64f38db3e253aed49ee8e5e8ba16589889d2b7f809edb7/prek-0.4.10-py3-none-linux_armv6l.whl", hash = "sha256:023f302741d79301346c3088ba43a9592aff0ecdbe5ddc3019fa9b1183319c5e", size = 5694609, upload-time = "2026-07-16T10:12:26.352Z" }, + { url = "https://files.pythonhosted.org/packages/0b/ef/ee9e6bf9a5ce242e9e4e66ac4e2e9042a0f6fd9f367cee18ad404456e93d/prek-0.4.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:72adc707e16f97564bbae08d22b222ac3bb2491f8fbfb5a0754f80d472c28a71", size = 6044037, upload-time = "2026-07-16T10:12:28.539Z" }, + { url = "https://files.pythonhosted.org/packages/68/7e/da08cc39e5348ccb9234e63a21ee56861f72e8497d6a78f0db1ccae6515d/prek-0.4.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:04c9321957e1b32e1fc7cf60bb4f90bba3761f8659d5551ed04f96e25596de49", size = 5535983, upload-time = "2026-07-16T10:12:30.691Z" }, + { url = "https://files.pythonhosted.org/packages/30/c6/0486a35bb687a9beac7a5810bd1104c6da56d469b30b1eeaeefd03c99da2/prek-0.4.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:e66ccf6c5e4ebadd05cd98cb338d7f553e4d27aa243cf91279c5a569b3cdccc7", size = 5862085, upload-time = "2026-07-16T10:12:33.042Z" }, + { url = "https://files.pythonhosted.org/packages/52/39/277fe17ae1f121e532e3942456f5a6d01ddacfbc550e481dcb359be7a1b0/prek-0.4.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:63f9061d75a50ef0ca92c4b596ad352937a845df80758244950e513b27e9e18f", size = 5605697, upload-time = "2026-07-16T10:12:35.498Z" }, + { url = "https://files.pythonhosted.org/packages/ad/a1/08354af3e000f2656fad086690d834eab6c04631ff41313a219ea6232199/prek-0.4.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c2ff7110e4bfaafbbab13c2893a337081aca61ed797f14b6b224d2ea9741eef", size = 6034111, upload-time = "2026-07-16T10:12:37.545Z" }, + { url = "https://files.pythonhosted.org/packages/e4/74/4702396c8d486132e5ce009ab56a0b37f50cb6866830d371f2617b7bdfdc/prek-0.4.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b696a05542e79aa27bcce68d1792e77f4fe6f9c6b012b34d74d62f964f3c72d", size = 6787203, upload-time = "2026-07-16T10:12:40.031Z" }, + { url = "https://files.pythonhosted.org/packages/90/29/b5d5d6fb87ebd64b37471e3e79761de9983f85e14d69c522efe7af6620ce/prek-0.4.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:431b44d6054e72815b4b05e1173596dfd02a7f7461211d40a2e3117e414642ad", size = 6261333, upload-time = "2026-07-16T10:12:42.216Z" }, + { url = "https://files.pythonhosted.org/packages/94/d6/54ba696d19f7efdc184093353cce713a850aef9c3556e23faeecafa22e94/prek-0.4.10-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:ccbd2b4fd1df790087ba18b4506f680471922a5f13714f19801568434a040dee", size = 5867761, upload-time = "2026-07-16T10:12:44.329Z" }, + { url = "https://files.pythonhosted.org/packages/be/7d/3975098aa2baaabfc10f99f9fcf78045c4f10851beed8e9812b6a2688eab/prek-0.4.10-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:479e7480b447191aa5c6ed67e80f081d0f5ee4e878b140f4d2cee44165395f1c", size = 5714412, upload-time = "2026-07-16T10:12:46.297Z" }, + { url = "https://files.pythonhosted.org/packages/97/c0/3e0aac190fe95fdef98526343559b61d4d9fd54444c8c9137ba02412afe1/prek-0.4.10-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:0bb7451025cbd2b68e480a13cf665d7a5c87c8b87bf18549a78985c17df817ed", size = 5578145, upload-time = "2026-07-16T10:12:48.261Z" }, + { url = "https://files.pythonhosted.org/packages/d7/44/7b26035534204b8b8a9d5e625479201e616413d287262f557cb32e1f8d77/prek-0.4.10-py3-none-musllinux_1_1_i686.whl", hash = "sha256:4fb047e5776676805794574b2d7b178cb3ab536793aadf172419fcda56b34a57", size = 5889245, upload-time = "2026-07-16T10:12:50.818Z" }, + { url = "https://files.pythonhosted.org/packages/7e/6c/178a9d768876b4211a1bf63907fe308ae02d173639bcf41cea3c5eed35c1/prek-0.4.10-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:08318818d19caf79643babb89f872c92fda134a622b4731df1d6ed61e29d2d26", size = 6372849, upload-time = "2026-07-16T10:12:52.952Z" }, + { url = "https://files.pythonhosted.org/packages/4d/84/d5f5ac8193602883f9dd1d675d9d4084e34fbe3ed2ef50a0c336d8a53d8f/prek-0.4.10-py3-none-win32.whl", hash = "sha256:092872714dcde480a662bbdd98b980b248c2d3e10543d4d53a3a58cc9e5b35b0", size = 5413005, upload-time = "2026-07-16T10:12:55.113Z" }, + { url = "https://files.pythonhosted.org/packages/41/63/9e648fda10bc02c9b6ba305f93b6a6e4fd37d23d13a269a9d2d6bb44eaa1/prek-0.4.10-py3-none-win_amd64.whl", hash = "sha256:3d323a18d0f8c50e474a8fa29fb93bd2db680116d8afb19b76e72ad4667f58e6", size = 5799075, upload-time = "2026-07-16T10:12:56.963Z" }, + { url = "https://files.pythonhosted.org/packages/22/74/b34d8c80cec8dccc7b922c75b9dca62b18b603b5ed2eea93c9d7c2928d2d/prek-0.4.10-py3-none-win_arm64.whl", hash = "sha256:5e93865ef96756c4a26f37ece04ad514abbc19ae6a23ed1a507b6314e6a0d2fb", size = 5563955, upload-time = "2026-07-16T10:12:59.07Z" }, ] [[package]] name = "prometheus-client" -version = "0.24.1" +version = "0.25.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f0/58/a794d23feb6b00fc0c72787d7e87d872a6730dd9ed7c7b3e954637d8f280/prometheus_client-0.24.1.tar.gz", hash = "sha256:7e0ced7fbbd40f7b84962d5d2ab6f17ef88a72504dcf7c0b40737b43b2a461f9", size = 85616, upload-time = "2026-01-14T15:26:26.965Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/fb/d9aa83ffe43ce1f19e557c0971d04b90561b0cfd50762aafb01968285553/prometheus_client-0.25.0.tar.gz", hash = "sha256:5e373b75c31afb3c86f1a52fa1ad470c9aace18082d39ec0d2f918d11cc9ba28", size = 86035, upload-time = "2026-04-09T19:53:42.359Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/74/c3/24a2f845e3917201628ecaba4f18bab4d18a337834c1df2a159ee9d22a42/prometheus_client-0.24.1-py3-none-any.whl", hash = "sha256:150db128af71a5c2482b36e588fc8a6b95e498750da4b17065947c16070f4055", size = 64057, upload-time = "2026-01-14T15:26:24.42Z" }, + { url = "https://files.pythonhosted.org/packages/8d/9b/d4b1e644385499c8346fa9b622a3f030dce14cd6ef8a1871c221a17a67e7/prometheus_client-0.25.0-py3-none-any.whl", hash = "sha256:d5aec89e349a6ec230805d0df882f3807f74fd6c1a2fa86864e3c2279059fed1", size = 64154, upload-time = "2026-04-09T19:53:41.324Z" }, ] [[package]] @@ -2044,17 +2024,17 @@ wheels = [ [[package]] name = "protobuf" -version = "6.33.6" +version = "7.35.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/66/70/e908e9c5e52ef7c3a6c7902c9dfbb34c7e29c25d2f81ade3856445fd5c94/protobuf-6.33.6.tar.gz", hash = "sha256:a6768d25248312c297558af96a9f9c929e8c4cee0659cb07e780731095f38135", size = 444531, upload-time = "2026-03-18T19:05:00.988Z" } +sdist = { url = "https://files.pythonhosted.org/packages/da/01/9ef0afd7999eb9badb3a768b4aedd78c86d4c65cfaf1958ab276199e76b4/protobuf-7.35.1.tar.gz", hash = "sha256:ce115a26fe0c39a2c29973d914d327e516a6455464489fe3cd1e51a1b354f81a", size = 458717, upload-time = "2026-06-11T21:55:40.257Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fc/9f/2f509339e89cfa6f6a4c4ff50438db9ca488dec341f7e454adad60150b00/protobuf-6.33.6-cp310-abi3-win32.whl", hash = "sha256:7d29d9b65f8afef196f8334e80d6bc1d5d4adedb449971fefd3723824e6e77d3", size = 425739, upload-time = "2026-03-18T19:04:48.373Z" }, - { url = "https://files.pythonhosted.org/packages/76/5d/683efcd4798e0030c1bab27374fd13a89f7c2515fb1f3123efdfaa5eab57/protobuf-6.33.6-cp310-abi3-win_amd64.whl", hash = "sha256:0cd27b587afca21b7cfa59a74dcbd48a50f0a6400cfb59391340ad729d91d326", size = 437089, upload-time = "2026-03-18T19:04:50.381Z" }, - { url = "https://files.pythonhosted.org/packages/5c/01/a3c3ed5cd186f39e7880f8303cc51385a198a81469d53d0fdecf1f64d929/protobuf-6.33.6-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:9720e6961b251bde64edfdab7d500725a2af5280f3f4c87e57c0208376aa8c3a", size = 427737, upload-time = "2026-03-18T19:04:51.866Z" }, - { url = "https://files.pythonhosted.org/packages/ee/90/b3c01fdec7d2f627b3a6884243ba328c1217ed2d978def5c12dc50d328a3/protobuf-6.33.6-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:e2afbae9b8e1825e3529f88d514754e094278bb95eadc0e199751cdd9a2e82a2", size = 324610, upload-time = "2026-03-18T19:04:53.096Z" }, - { url = "https://files.pythonhosted.org/packages/9b/ca/25afc144934014700c52e05103c2421997482d561f3101ff352e1292fb81/protobuf-6.33.6-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:c96c37eec15086b79762ed265d59ab204dabc53056e3443e702d2681f4b39ce3", size = 339381, upload-time = "2026-03-18T19:04:54.616Z" }, - { url = "https://files.pythonhosted.org/packages/16/92/d1e32e3e0d894fe00b15ce28ad4944ab692713f2e7f0a99787405e43533a/protobuf-6.33.6-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:e9db7e292e0ab79dd108d7f1a94fe31601ce1ee3f7b79e0692043423020b0593", size = 323436, upload-time = "2026-03-18T19:04:55.768Z" }, - { url = "https://files.pythonhosted.org/packages/c4/72/02445137af02769918a93807b2b7890047c32bfb9f90371cbc12688819eb/protobuf-6.33.6-py3-none-any.whl", hash = "sha256:77179e006c476e69bf8e8ce866640091ec42e1beb80b213c3900006ecfba6901", size = 170656, upload-time = "2026-03-18T19:04:59.826Z" }, + { url = "https://files.pythonhosted.org/packages/10/03/8aeeb7458d22546bf64b5250ca1daeb5ff757d900e8e4a7476c6f0db843e/protobuf-7.35.1-cp310-abi3-macosx_10_9_universal2.whl", hash = "sha256:24f857477359a85c0c235261b8ba905fd51b2562f4a64ca1df5473f29850cbf6", size = 433226, upload-time = "2026-06-11T21:55:31.719Z" }, + { url = "https://files.pythonhosted.org/packages/37/4b/dfb89eb0e652a1ff073c39a59fb5e3a83cfe9b57a2c83fa6d78270101767/protobuf-7.35.1-cp310-abi3-manylinux2014_aarch64.whl", hash = "sha256:11d6b0ec246892d85215b0a13ca6e0233cf5284b68f0ac02646427f4ff88a799", size = 328847, upload-time = "2026-06-11T21:55:34.035Z" }, + { url = "https://files.pythonhosted.org/packages/0f/58/dc12f2cd484951524af6e3382c785869b9b3fb5e52ee95ae23add53ee8f9/protobuf-7.35.1-cp310-abi3-manylinux2014_s390x.whl", hash = "sha256:b73f9489a4b8b1c9cb1f8ed951c736392592edb24b9d6819f36d2e10b171d5b4", size = 344030, upload-time = "2026-06-11T21:55:34.941Z" }, + { url = "https://files.pythonhosted.org/packages/e4/be/5b3cfe508bfab6761414ff944e3366eb13be4fd71efcd69450f89ba39f43/protobuf-7.35.1-cp310-abi3-manylinux2014_x86_64.whl", hash = "sha256:74758715c53d7158fb76caf4f0cfdacc5329a4b1bb994f865d6cf302d413a1c4", size = 327130, upload-time = "2026-06-11T21:55:35.921Z" }, + { url = "https://files.pythonhosted.org/packages/d8/bc/6d6c7ba8709c85f8f2c390b2b118d6fb08a783676a572271851bf45a7d22/protobuf-7.35.1-cp310-abi3-win32.whl", hash = "sha256:353652e4efd0bca5b5fc2656abf8307ef351f0cf938c9eba09f0e09c20a25c30", size = 428945, upload-time = "2026-06-11T21:55:37.034Z" }, + { url = "https://files.pythonhosted.org/packages/0a/19/8d0cb6f20a1ef7b18f1c8986ad5783f22f84cce39c6ce9a6e645ea55192e/protobuf-7.35.1-cp310-abi3-win_amd64.whl", hash = "sha256:230a75ddfc2de4806e56696ce9640c1cdfdb6543b7cfce98d42a4c0a0e7bdb87", size = 439996, upload-time = "2026-06-11T21:55:38.123Z" }, + { url = "https://files.pythonhosted.org/packages/19/c7/5f7c636ec43e0c545e28d1f1db71990108306f7bdcb89f069ba97e428e7f/protobuf-7.35.1-py3-none-any.whl", hash = "sha256:4bc97768d8fe4ad6743c8a19403e314511ed9f6d13205b687e52421c023ac1b9", size = 171659, upload-time = "2026-06-11T21:55:39.155Z" }, ] [[package]] @@ -2105,20 +2085,21 @@ wheels = [ [[package]] name = "py-key-value-aio" -version = "0.4.4" +version = "0.4.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "beartype" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/04/3c/0397c072a38d4bc580994b42e0c90c5f44f679303489e4376289534735e5/py_key_value_aio-0.4.4.tar.gz", hash = "sha256:e3012e6243ed7cc09bb05457bd4d03b1ba5c2b1ca8700096b3927db79ffbbe55", size = 92300, upload-time = "2026-02-16T21:21:43.245Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fb/e2/d689d922894a7ecde73b6daeaf9b13dab5aae06fe6aaaf7514722644d382/py_key_value_aio-0.4.5.tar.gz", hash = "sha256:c6563a2c6abe5da5e20f4f9e875c2a9b425a2244a54fadbf46cf140a9eea45d7", size = 107547, upload-time = "2026-05-27T16:37:08.107Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/32/69/f1b537ee70b7def42d63124a539ed3026a11a3ffc3086947a1ca6e861868/py_key_value_aio-0.4.4-py3-none-any.whl", hash = "sha256:18e17564ecae61b987f909fc2cd41ee2012c84b4b1dcb8c055cf8b4bc1bf3f5d", size = 152291, upload-time = "2026-02-16T21:21:44.241Z" }, + { url = "https://files.pythonhosted.org/packages/f6/95/b8ba862968712caa12a19666175334fa979e1f198b896a430adb3bacfe87/py_key_value_aio-0.4.5-py3-none-any.whl", hash = "sha256:ab862adbcb8c72547d1c57821f22cbbb71ab86509039c96f36e914e0336c8dd7", size = 170005, upload-time = "2026-05-27T16:37:06.629Z" }, ] [package.optional-dependencies] filetree = [ - { name = "aiofile" }, + { name = "aiofile", version = "3.9.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "aiofile", version = "3.11.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "anyio" }, ] keyring = [ @@ -2133,11 +2114,11 @@ redis = [ [[package]] name = "pyasn1" -version = "0.6.3" +version = "0.6.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/5c/5f/6583902b6f79b399c9c40674ac384fd9cd77805f9e6205075f828ef11fb2/pyasn1-0.6.3.tar.gz", hash = "sha256:697a8ecd6d98891189184ca1fa05d1bb00e2f84b5977c481452050549c8a72cf", size = 148685, upload-time = "2026-03-17T01:06:53.382Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a4/9a/23310166d960def5897e91fe20e5b724601b02a22e84ba1f94232c0b7f67/pyasn1-0.6.4.tar.gz", hash = "sha256:9c447d8431c947fe4c8febc4ed9e760bc29011a5b01e5c74b67025bd9fb8ce81", size = 151262, upload-time = "2026-07-09T01:12:33.988Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5d/a0/7d793dce3fa811fe047d6ae2431c672364b462850c6235ae306c0efd025f/pyasn1-0.6.3-py3-none-any.whl", hash = "sha256:a80184d120f0864a52a073acc6fc642847d0be408e7c7252f31390c0f4eadcde", size = 83997, upload-time = "2026-03-17T01:06:52.036Z" }, + { url = "https://files.pythonhosted.org/packages/9a/3b/6163796d69c3977d1e4287bea4a6979161cbbdd170ebb430511e8e1999ce/pyasn1-0.6.4-py3-none-any.whl", hash = "sha256:deda9277cfd454080ec40b207fb6df82206a3a2688735233cdcd8d3d565f088b", size = 84410, upload-time = "2026-07-09T01:12:32.92Z" }, ] [[package]] @@ -2163,7 +2144,7 @@ wheels = [ [[package]] name = "pydantic" -version = "2.12.5" +version = "2.13.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-types" }, @@ -2171,9 +2152,9 @@ dependencies = [ { name = "typing-extensions" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" } +sdist = { url = "https://files.pythonhosted.org/packages/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775, upload-time = "2026-05-06T13:43:05.343Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" }, + { url = "https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba", size = 472262, upload-time = "2026-05-06T13:43:02.641Z" }, ] [package.optional-dependencies] @@ -2183,120 +2164,118 @@ email = [ [[package]] name = "pydantic-core" -version = "2.41.5" +version = "2.46.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c6/90/32c9941e728d564b411d574d8ee0cf09b12ec978cb22b294995bae5549a5/pydantic_core-2.41.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:77b63866ca88d804225eaa4af3e664c5faf3568cea95360d21f4725ab6e07146", size = 2107298, upload-time = "2025-11-04T13:39:04.116Z" }, - { url = "https://files.pythonhosted.org/packages/fb/a8/61c96a77fe28993d9a6fb0f4127e05430a267b235a124545d79fea46dd65/pydantic_core-2.41.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dfa8a0c812ac681395907e71e1274819dec685fec28273a28905df579ef137e2", size = 1901475, upload-time = "2025-11-04T13:39:06.055Z" }, - { url = "https://files.pythonhosted.org/packages/5d/b6/338abf60225acc18cdc08b4faef592d0310923d19a87fba1faf05af5346e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5921a4d3ca3aee735d9fd163808f5e8dd6c6972101e4adbda9a4667908849b97", size = 1918815, upload-time = "2025-11-04T13:39:10.41Z" }, - { url = "https://files.pythonhosted.org/packages/d1/1c/2ed0433e682983d8e8cba9c8d8ef274d4791ec6a6f24c58935b90e780e0a/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e25c479382d26a2a41b7ebea1043564a937db462816ea07afa8a44c0866d52f9", size = 2065567, upload-time = "2025-11-04T13:39:12.244Z" }, - { url = "https://files.pythonhosted.org/packages/b3/24/cf84974ee7d6eae06b9e63289b7b8f6549d416b5c199ca2d7ce13bbcf619/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f547144f2966e1e16ae626d8ce72b4cfa0caedc7fa28052001c94fb2fcaa1c52", size = 2230442, upload-time = "2025-11-04T13:39:13.962Z" }, - { url = "https://files.pythonhosted.org/packages/fd/21/4e287865504b3edc0136c89c9c09431be326168b1eb7841911cbc877a995/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f52298fbd394f9ed112d56f3d11aabd0d5bd27beb3084cc3d8ad069483b8941", size = 2350956, upload-time = "2025-11-04T13:39:15.889Z" }, - { url = "https://files.pythonhosted.org/packages/a8/76/7727ef2ffa4b62fcab916686a68a0426b9b790139720e1934e8ba797e238/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:100baa204bb412b74fe285fb0f3a385256dad1d1879f0a5cb1499ed2e83d132a", size = 2068253, upload-time = "2025-11-04T13:39:17.403Z" }, - { url = "https://files.pythonhosted.org/packages/d5/8c/a4abfc79604bcb4c748e18975c44f94f756f08fb04218d5cb87eb0d3a63e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:05a2c8852530ad2812cb7914dc61a1125dc4e06252ee98e5638a12da6cc6fb6c", size = 2177050, upload-time = "2025-11-04T13:39:19.351Z" }, - { url = "https://files.pythonhosted.org/packages/67/b1/de2e9a9a79b480f9cb0b6e8b6ba4c50b18d4e89852426364c66aa82bb7b3/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:29452c56df2ed968d18d7e21f4ab0ac55e71dc59524872f6fc57dcf4a3249ed2", size = 2147178, upload-time = "2025-11-04T13:39:21Z" }, - { url = "https://files.pythonhosted.org/packages/16/c1/dfb33f837a47b20417500efaa0378adc6635b3c79e8369ff7a03c494b4ac/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:d5160812ea7a8a2ffbe233d8da666880cad0cbaf5d4de74ae15c313213d62556", size = 2341833, upload-time = "2025-11-04T13:39:22.606Z" }, - { url = "https://files.pythonhosted.org/packages/47/36/00f398642a0f4b815a9a558c4f1dca1b4020a7d49562807d7bc9ff279a6c/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:df3959765b553b9440adfd3c795617c352154e497a4eaf3752555cfb5da8fc49", size = 2321156, upload-time = "2025-11-04T13:39:25.843Z" }, - { url = "https://files.pythonhosted.org/packages/7e/70/cad3acd89fde2010807354d978725ae111ddf6d0ea46d1ea1775b5c1bd0c/pydantic_core-2.41.5-cp310-cp310-win32.whl", hash = "sha256:1f8d33a7f4d5a7889e60dc39856d76d09333d8a6ed0f5f1190635cbec70ec4ba", size = 1989378, upload-time = "2025-11-04T13:39:27.92Z" }, - { url = "https://files.pythonhosted.org/packages/76/92/d338652464c6c367e5608e4488201702cd1cbb0f33f7b6a85a60fe5f3720/pydantic_core-2.41.5-cp310-cp310-win_amd64.whl", hash = "sha256:62de39db01b8d593e45871af2af9e497295db8d73b085f6bfd0b18c83c70a8f9", size = 2013622, upload-time = "2025-11-04T13:39:29.848Z" }, - { url = "https://files.pythonhosted.org/packages/e8/72/74a989dd9f2084b3d9530b0915fdda64ac48831c30dbf7c72a41a5232db8/pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6", size = 2105873, upload-time = "2025-11-04T13:39:31.373Z" }, - { url = "https://files.pythonhosted.org/packages/12/44/37e403fd9455708b3b942949e1d7febc02167662bf1a7da5b78ee1ea2842/pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b", size = 1899826, upload-time = "2025-11-04T13:39:32.897Z" }, - { url = "https://files.pythonhosted.org/packages/33/7f/1d5cab3ccf44c1935a359d51a8a2a9e1a654b744b5e7f80d41b88d501eec/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a", size = 1917869, upload-time = "2025-11-04T13:39:34.469Z" }, - { url = "https://files.pythonhosted.org/packages/6e/6a/30d94a9674a7fe4f4744052ed6c5e083424510be1e93da5bc47569d11810/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8", size = 2063890, upload-time = "2025-11-04T13:39:36.053Z" }, - { url = "https://files.pythonhosted.org/packages/50/be/76e5d46203fcb2750e542f32e6c371ffa9b8ad17364cf94bb0818dbfb50c/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e", size = 2229740, upload-time = "2025-11-04T13:39:37.753Z" }, - { url = "https://files.pythonhosted.org/packages/d3/ee/fed784df0144793489f87db310a6bbf8118d7b630ed07aa180d6067e653a/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1", size = 2350021, upload-time = "2025-11-04T13:39:40.94Z" }, - { url = "https://files.pythonhosted.org/packages/c8/be/8fed28dd0a180dca19e72c233cbf58efa36df055e5b9d90d64fd1740b828/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b", size = 2066378, upload-time = "2025-11-04T13:39:42.523Z" }, - { url = "https://files.pythonhosted.org/packages/b0/3b/698cf8ae1d536a010e05121b4958b1257f0b5522085e335360e53a6b1c8b/pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b", size = 2175761, upload-time = "2025-11-04T13:39:44.553Z" }, - { url = "https://files.pythonhosted.org/packages/b8/ba/15d537423939553116dea94ce02f9c31be0fa9d0b806d427e0308ec17145/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284", size = 2146303, upload-time = "2025-11-04T13:39:46.238Z" }, - { url = "https://files.pythonhosted.org/packages/58/7f/0de669bf37d206723795f9c90c82966726a2ab06c336deba4735b55af431/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594", size = 2340355, upload-time = "2025-11-04T13:39:48.002Z" }, - { url = "https://files.pythonhosted.org/packages/e5/de/e7482c435b83d7e3c3ee5ee4451f6e8973cff0eb6007d2872ce6383f6398/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e", size = 2319875, upload-time = "2025-11-04T13:39:49.705Z" }, - { url = "https://files.pythonhosted.org/packages/fe/e6/8c9e81bb6dd7560e33b9053351c29f30c8194b72f2d6932888581f503482/pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b", size = 1987549, upload-time = "2025-11-04T13:39:51.842Z" }, - { url = "https://files.pythonhosted.org/packages/11/66/f14d1d978ea94d1bc21fc98fcf570f9542fe55bfcc40269d4e1a21c19bf7/pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe", size = 2011305, upload-time = "2025-11-04T13:39:53.485Z" }, - { url = "https://files.pythonhosted.org/packages/56/d8/0e271434e8efd03186c5386671328154ee349ff0354d83c74f5caaf096ed/pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f", size = 1972902, upload-time = "2025-11-04T13:39:56.488Z" }, - { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" }, - { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" }, - { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" }, - { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" }, - { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" }, - { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" }, - { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" }, - { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" }, - { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" }, - { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" }, - { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" }, - { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" }, - { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" }, - { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" }, - { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" }, - { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" }, - { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" }, - { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" }, - { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" }, - { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" }, - { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" }, - { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" }, - { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" }, - { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" }, - { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" }, - { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" }, - { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" }, - { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" }, - { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" }, - { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" }, - { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" }, - { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" }, - { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" }, - { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" }, - { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" }, - { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" }, - { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" }, - { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" }, - { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" }, - { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" }, - { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" }, - { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" }, - { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" }, - { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" }, - { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" }, - { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" }, - { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" }, - { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" }, - { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" }, - { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" }, - { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" }, - { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" }, - { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" }, - { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" }, - { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" }, - { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" }, - { url = "https://files.pythonhosted.org/packages/11/72/90fda5ee3b97e51c494938a4a44c3a35a9c96c19bba12372fb9c634d6f57/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034", size = 2115441, upload-time = "2025-11-04T13:42:39.557Z" }, - { url = "https://files.pythonhosted.org/packages/1f/53/8942f884fa33f50794f119012dc6a1a02ac43a56407adaac20463df8e98f/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", size = 1930291, upload-time = "2025-11-04T13:42:42.169Z" }, - { url = "https://files.pythonhosted.org/packages/79/c8/ecb9ed9cd942bce09fc888ee960b52654fbdbede4ba6c2d6e0d3b1d8b49c/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", size = 1948632, upload-time = "2025-11-04T13:42:44.564Z" }, - { url = "https://files.pythonhosted.org/packages/2e/1b/687711069de7efa6af934e74f601e2a4307365e8fdc404703afc453eab26/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad", size = 2138905, upload-time = "2025-11-04T13:42:47.156Z" }, - { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" }, - { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" }, - { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" }, - { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" }, - { url = "https://files.pythonhosted.org/packages/e6/b0/1a2aa41e3b5a4ba11420aba2d091b2d17959c8d1519ece3627c371951e73/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b5819cd790dbf0c5eb9f82c73c16b39a65dd6dd4d1439dcdea7816ec9adddab8", size = 2103351, upload-time = "2025-11-04T13:43:02.058Z" }, - { url = "https://files.pythonhosted.org/packages/a4/ee/31b1f0020baaf6d091c87900ae05c6aeae101fa4e188e1613c80e4f1ea31/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5a4e67afbc95fa5c34cf27d9089bca7fcab4e51e57278d710320a70b956d1b9a", size = 1925363, upload-time = "2025-11-04T13:43:05.159Z" }, - { url = "https://files.pythonhosted.org/packages/e1/89/ab8e86208467e467a80deaca4e434adac37b10a9d134cd2f99b28a01e483/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ece5c59f0ce7d001e017643d8d24da587ea1f74f6993467d85ae8a5ef9d4f42b", size = 2135615, upload-time = "2025-11-04T13:43:08.116Z" }, - { url = "https://files.pythonhosted.org/packages/99/0a/99a53d06dd0348b2008f2f30884b34719c323f16c3be4e6cc1203b74a91d/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:16f80f7abe3351f8ea6858914ddc8c77e02578544a0ebc15b4c2e1a0e813b0b2", size = 2175369, upload-time = "2025-11-04T13:43:12.49Z" }, - { url = "https://files.pythonhosted.org/packages/6d/94/30ca3b73c6d485b9bb0bc66e611cff4a7138ff9736b7e66bcf0852151636/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:33cb885e759a705b426baada1fe68cbb0a2e68e34c5d0d0289a364cf01709093", size = 2144218, upload-time = "2025-11-04T13:43:15.431Z" }, - { url = "https://files.pythonhosted.org/packages/87/57/31b4f8e12680b739a91f472b5671294236b82586889ef764b5fbc6669238/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:c8d8b4eb992936023be7dee581270af5c6e0697a8559895f527f5b7105ecd36a", size = 2329951, upload-time = "2025-11-04T13:43:18.062Z" }, - { url = "https://files.pythonhosted.org/packages/7d/73/3c2c8edef77b8f7310e6fb012dbc4b8551386ed575b9eb6fb2506e28a7eb/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:242a206cd0318f95cd21bdacff3fcc3aab23e79bba5cac3db5a841c9ef9c6963", size = 2318428, upload-time = "2025-11-04T13:43:20.679Z" }, - { url = "https://files.pythonhosted.org/packages/2f/02/8559b1f26ee0d502c74f9cca5c0d2fd97e967e083e006bbbb4e97f3a043a/pydantic_core-2.41.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d3a978c4f57a597908b7e697229d996d77a6d3c94901e9edee593adada95ce1a", size = 2147009, upload-time = "2025-11-04T13:43:23.286Z" }, - { url = "https://files.pythonhosted.org/packages/5f/9b/1b3f0e9f9305839d7e84912f9e8bfbd191ed1b1ef48083609f0dabde978c/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26", size = 2101980, upload-time = "2025-11-04T13:43:25.97Z" }, - { url = "https://files.pythonhosted.org/packages/a4/ed/d71fefcb4263df0da6a85b5d8a7508360f2f2e9b3bf5814be9c8bccdccc1/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", size = 1923865, upload-time = "2025-11-04T13:43:28.763Z" }, - { url = "https://files.pythonhosted.org/packages/ce/3a/626b38db460d675f873e4444b4bb030453bbe7b4ba55df821d026a0493c4/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", size = 2134256, upload-time = "2025-11-04T13:43:31.71Z" }, - { url = "https://files.pythonhosted.org/packages/83/d9/8412d7f06f616bbc053d30cb4e5f76786af3221462ad5eee1f202021eb4e/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1", size = 2174762, upload-time = "2025-11-04T13:43:34.744Z" }, - { url = "https://files.pythonhosted.org/packages/55/4c/162d906b8e3ba3a99354e20faa1b49a85206c47de97a639510a0e673f5da/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84", size = 2143141, upload-time = "2025-11-04T13:43:37.701Z" }, - { url = "https://files.pythonhosted.org/packages/1f/f2/f11dd73284122713f5f89fc940f370d035fa8e1e078d446b3313955157fe/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770", size = 2330317, upload-time = "2025-11-04T13:43:40.406Z" }, - { url = "https://files.pythonhosted.org/packages/88/9d/b06ca6acfe4abb296110fb1273a4d848a0bfb2ff65f3ee92127b3244e16b/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f", size = 2316992, upload-time = "2025-11-04T13:43:43.602Z" }, - { url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302, upload-time = "2025-11-04T13:43:46.64Z" }, + { url = "https://files.pythonhosted.org/packages/e7/08/f1ba952f1c8ae5581c70fa9c6da89f247b83e3dd8c09c035d5d7931fc23d/pydantic_core-2.46.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:a396dcc17e5a0b164dbe026896245a4fa9ff402edca1dff0be3d53a517f74de4", size = 2113146, upload-time = "2026-05-06T13:37:36.537Z" }, + { url = "https://files.pythonhosted.org/packages/56/c6/65f646c7ff09bd257f660434adb45c4dfcbbcebcc030562fecf6f5bf887d/pydantic_core-2.46.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:da4b951fe36dc7c3a1ccb4e3cd1747c3542b8c9ceede8fc86cae054e764485f5", size = 1949769, upload-time = "2026-05-06T13:37:46.365Z" }, + { url = "https://files.pythonhosted.org/packages/64/ba/bfb1d928fd5b49e1258935ff104ae356e9fd89384a55bf9f847e9193ad40/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb63e0198ca18aad131c089b9204c23079c3afa95487e561f4c522d519e55aba", size = 1974958, upload-time = "2026-05-06T13:37:28.611Z" }, + { url = "https://files.pythonhosted.org/packages/4e/74/76223bfb117b64af743c9b6670d1364516f5c0604f96b48f3272f6af6cc6/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f47286a97f0bc9b8859519809077b91b2cefe4ae47fcbf5e466a009c1c5d742b", size = 2042118, upload-time = "2026-05-06T13:36:55.216Z" }, + { url = "https://files.pythonhosted.org/packages/cb/7b/848732968bc8f48f3187542f08358b9d842db564147b256669426ebb1652/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:905a0ed8ea6f2d61c1738835f99b699348d7857379083e5fc497fa0c967a407c", size = 2222876, upload-time = "2026-05-06T13:38:25.455Z" }, + { url = "https://files.pythonhosted.org/packages/b5/2f/e90b63ee2e14bd8d3db8f705a6d75d64e6ee1b7c2c8833747ce706e1e0ce/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea793e075b70290d89d8142074262885d3f7da19634845135751bd6344f73b50", size = 2286703, upload-time = "2026-05-06T13:37:53.304Z" }, + { url = "https://files.pythonhosted.org/packages/ba/1e/acc4d70f88a0a277e4a1fa77ebb985ceabaf900430f875bf9338e11c9420/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:395aebd9183f9d112f569aeb5b2214d1a10a33bec8456447f7fbdfa51d38d4cd", size = 2092042, upload-time = "2026-05-06T13:38:46.981Z" }, + { url = "https://files.pythonhosted.org/packages/a9/da/0a422b57bf8504102bf3c4ccea9c41bab5a5cee6a54650acf8faf67f5a24/pydantic_core-2.46.4-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:b078afbc25f3a1436c7a1d2cd3e322497ee99615ba97c563566fdf46aff1ee01", size = 2117231, upload-time = "2026-05-06T13:39:23.146Z" }, + { url = "https://files.pythonhosted.org/packages/bd/2a/2ac13c3af305843e23c5078c53d135656b3f05a2fd78cb7bbbb12e97b473/pydantic_core-2.46.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f747929cf940cddb5b3668a390056ddd5ba2e5010615ea2dcf4f9c4f3ab8791d", size = 2168388, upload-time = "2026-05-06T13:40:08.06Z" }, + { url = "https://files.pythonhosted.org/packages/72/04/2beacf7e1607e93eefe4aed1b4709f079b905fb77530179d4f7c71745f22/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:daa27d92c36f24388fe3ad306b174781c747627f134452e4f128ea00ce1fe8c4", size = 2184769, upload-time = "2026-05-06T13:38:13.901Z" }, + { url = "https://files.pythonhosted.org/packages/9e/29/d2b9fd9f539133548eaf622c06a4ce176cb46ac59f32d0359c4abc0de047/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:19e51f073cd3df251856a8a4189fbdf1de4012c3ebacfb1884f94f1eb406079f", size = 2319312, upload-time = "2026-05-06T13:39:08.24Z" }, + { url = "https://files.pythonhosted.org/packages/7c/af/0f7a5b85fec6075bea96e3ef9187de38fccced0de92c1e7feda8d5cc7bb9/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c1747f85cee84c26985853c6f3d9bd3e75da5212912443fa111c113b9c246f39", size = 2361817, upload-time = "2026-05-06T13:38:43.2Z" }, + { url = "https://files.pythonhosted.org/packages/25/a4/73363fec545fd3ec025490bdda2743c56d0dd5b6266b1a53bbe9e4265375/pydantic_core-2.46.4-cp310-cp310-win32.whl", hash = "sha256:2f84c03c8607173d16b5a854ec68a2f9079ae03237a54fb506d13af47e1d018d", size = 1987085, upload-time = "2026-05-06T13:39:25.497Z" }, + { url = "https://files.pythonhosted.org/packages/01/aa/62f082da2c91fac1c234bc9ee0066257ce83f0604abd72e4c9d5991f2d84/pydantic_core-2.46.4-cp310-cp310-win_amd64.whl", hash = "sha256:8358a950c8909158e3df31538a7e4edc2d7265a7c54b47f0864d9e5bae9dcebf", size = 2074311, upload-time = "2026-05-06T13:39:59.922Z" }, + { url = "https://files.pythonhosted.org/packages/5c/fa/6d7708d2cfc1a832acb6aeb0cd16e801902df8a0f583bb3b4b527fde022e/pydantic_core-2.46.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:0e96592440881c74a213e5ad528e2b24d3d4f940de2766bed9010ab1d9e51594", size = 2111872, upload-time = "2026-05-06T13:40:27.596Z" }, + { url = "https://files.pythonhosted.org/packages/ae/6f/aa064a3e74b5745afbdf250594f38e7ead05e2d651bcb35994b9417a0d4d/pydantic_core-2.46.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0d65b8c354be7fb5f720c3caa8bc940bc2d20ce749c8e06135f07f8ed95dd7c", size = 1948255, upload-time = "2026-05-06T13:39:12.574Z" }, + { url = "https://files.pythonhosted.org/packages/43/3a/41114a9f7569b84b4d84e7a018c57c56347dac30c0d4a872946ec4e36c46/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bfb192b3f4b9e8a89b6277b6ce787564f62cfd272055f6e685726b111dc7826", size = 1972827, upload-time = "2026-05-06T13:38:19.841Z" }, + { url = "https://files.pythonhosted.org/packages/ef/25/1ab42e8048fe551934d9884e8d64daa7e990ad386f310a15981aeb6a5b08/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9037063db01f09b09e237c282b6792bd4da634b5402c4e7f0c61effed7701a04", size = 2041051, upload-time = "2026-05-06T13:38:10.447Z" }, + { url = "https://files.pythonhosted.org/packages/94/c2/1a934597ddf08da410385b3b7aae91956a5a76c635effef456074fad7e88/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fc010ab034c8c7452522748bf937df58020d256ccae0874463d1f4d01758af8e", size = 2221314, upload-time = "2026-05-06T13:40:13.089Z" }, + { url = "https://files.pythonhosted.org/packages/02/6d/9e8ad178c9c4df27ad3c8f25d1fe2a7ab0d2ba0559fad4aee5d3d1f16771/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c5dac79fa1614d1e06ca695109c6105923bd9c7d1d6c918d4e637b7e6b32fd3", size = 2285146, upload-time = "2026-05-06T13:38:59.224Z" }, + { url = "https://files.pythonhosted.org/packages/80/50/540cd3aeefc041beb111125c4bff779831a2111fc6b15a9138cda277d32c/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9fa868638bf362d3d138ea55829cefb3d5f4b0d7f142234382a15e2485dbec4", size = 2089685, upload-time = "2026-05-06T13:38:17.762Z" }, + { url = "https://files.pythonhosted.org/packages/6b/a4/b440ad35f05f6a38f89fa0f149accb3f0e02be94ca5e15f3c449a61b4bc9/pydantic_core-2.46.4-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:17299feefe090f2caa5b8e37222bb5f663e4935a8bfa6931d4102e5df1a9f398", size = 2115420, upload-time = "2026-05-06T13:37:58.195Z" }, + { url = "https://files.pythonhosted.org/packages/99/61/de4f55db8dfd57bfdfa9a12ec90fe1b57c4f41062f7ca86f08586b3e0ac0/pydantic_core-2.46.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4c63ebc82684aa89d9a3bcbd13d515b3be44250dc68dd3bd81526c1cb31286c3", size = 2165122, upload-time = "2026-05-06T13:37:01.167Z" }, + { url = "https://files.pythonhosted.org/packages/f7/52/7c529d7bdb2d1068bd52f51fe32572c8301f9a4febf1948f10639f1436f5/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:aaa2a54443eff1950ba5ddc6b6ccda0d9c84a364276a62f969bdf2a390650848", size = 2182573, upload-time = "2026-05-06T13:38:45.04Z" }, + { url = "https://files.pythonhosted.org/packages/37/b3/7c40325848ba78247f2812dcf9c7274e38cd801820ca6dd9fe63bcfb0eb4/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:18e5ceec2ab67e6d5f1a9085e5a24c9c4e2ac4545730bfe668680bca05e555f3", size = 2317139, upload-time = "2026-05-06T13:37:15.539Z" }, + { url = "https://files.pythonhosted.org/packages/d9/37/f913f81a657c865b75da6c0dbed79876073c2a43b5bd9edbe8da785e4d49/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a0f62d0a58f4e7da165457e995725421e0064f2255d8eccebc49f41bbc23b109", size = 2360433, upload-time = "2026-05-06T13:37:30.099Z" }, + { url = "https://files.pythonhosted.org/packages/c4/67/6acaa1be2567f9256b056d8477158cac7240813956ce86e49deae8e173b4/pydantic_core-2.46.4-cp311-cp311-win32.whl", hash = "sha256:041bde0a48fd37cf71cab1c9d56d3e8625a3793fef1f7dd232b3ff37e978ecda", size = 1985513, upload-time = "2026-05-06T13:38:15.669Z" }, + { url = "https://files.pythonhosted.org/packages/aa/e6/c505f83dfeda9a2e5c995cfd872949e4d05e12f7feb3dca72f633daefa94/pydantic_core-2.46.4-cp311-cp311-win_amd64.whl", hash = "sha256:6f2eeda33a839975441c86a4119e1383c50b47faf0cbb5176985565c6bb02c33", size = 2071114, upload-time = "2026-05-06T13:40:35.416Z" }, + { url = "https://files.pythonhosted.org/packages/0f/da/7a263a96d965d9d0df5e8de8a475f33495451117035b09acb110288c381f/pydantic_core-2.46.4-cp311-cp311-win_arm64.whl", hash = "sha256:14f4c5d6db102bd796a627bbb3a17b4cf4574b9ae861d8b7c9a9661c6dd3362d", size = 2044298, upload-time = "2026-05-06T13:38:29.754Z" }, + { url = "https://files.pythonhosted.org/packages/ce/8c/af022f0af448d7747c5154288d46b5f2bc5f17366eaa0e23e9aa04d59f3b/pydantic_core-2.46.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2", size = 2106158, upload-time = "2026-05-06T13:38:57.215Z" }, + { url = "https://files.pythonhosted.org/packages/19/95/6195171e385007300f0f5574592e467c568becce2d937a0b6804f218bc49/pydantic_core-2.46.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f", size = 1951724, upload-time = "2026-05-06T13:37:02.697Z" }, + { url = "https://files.pythonhosted.org/packages/8e/bc/f47d1ff9cbb1620e1b5b697eef06010035735f07820180e74178226b27b3/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7", size = 1975742, upload-time = "2026-05-06T13:37:09.448Z" }, + { url = "https://files.pythonhosted.org/packages/5b/11/9b9a5b0306345664a2da6410877af6e8082481b5884b3ddd78d47c6013ce/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7", size = 2052418, upload-time = "2026-05-06T13:37:38.234Z" }, + { url = "https://files.pythonhosted.org/packages/f1/b7/a65fec226f5d78fc39f4a13c4cc0c768c22b113438f60c14adc9d2865038/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712", size = 2232274, upload-time = "2026-05-06T13:38:27.753Z" }, + { url = "https://files.pythonhosted.org/packages/68/f0/92039db98b907ef49269a8271f67db9cb78ae2fc68062ef7e4e77adb5f61/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4", size = 2309940, upload-time = "2026-05-06T13:38:05.353Z" }, + { url = "https://files.pythonhosted.org/packages/5f/97/2aab507d3d00ca626e8e57c1eac6a79e4e5fbcc63eb99733ff55d1717f65/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce", size = 2094516, upload-time = "2026-05-06T13:39:10.577Z" }, + { url = "https://files.pythonhosted.org/packages/22/37/a8aca44d40d737dde2bc05b3c6c07dff0de07ce6f82e9f3167aeaf4d5dea/pydantic_core-2.46.4-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987", size = 2136854, upload-time = "2026-05-06T13:40:22.59Z" }, + { url = "https://files.pythonhosted.org/packages/24/99/fcef1b79238c06a8cbec70819ac722ba76e02bc8ada9b0fd66eba40da01b/pydantic_core-2.46.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b", size = 2180306, upload-time = "2026-05-06T13:40:10.666Z" }, + { url = "https://files.pythonhosted.org/packages/ae/6c/fc44000918855b42779d007ae63b0532794739027b2f417321cddbc44f6a/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458", size = 2190044, upload-time = "2026-05-06T13:40:43.231Z" }, + { url = "https://files.pythonhosted.org/packages/6b/65/d9cadc9f1920d7a127ad2edba16c1db7916e59719285cd6c94600b0080ba/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b", size = 2329133, upload-time = "2026-05-06T13:39:57.365Z" }, + { url = "https://files.pythonhosted.org/packages/d0/cf/c873d91679f3a30bcf5e7ac280ce5573483e72295307685120d0d5ad3416/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c", size = 2374464, upload-time = "2026-05-06T13:38:06.976Z" }, + { url = "https://files.pythonhosted.org/packages/47/bd/6f2fc8188f31bf10590f1e98e7b306336161fac930a8c514cd7bd828c7dc/pydantic_core-2.46.4-cp312-cp312-win32.whl", hash = "sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894", size = 1974823, upload-time = "2026-05-06T13:40:47.985Z" }, + { url = "https://files.pythonhosted.org/packages/40/8c/985c1d41ea1107c2534abd9870e4ed5c8e7669b5c308297835c001e7a1c4/pydantic_core-2.46.4-cp312-cp312-win_amd64.whl", hash = "sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89", size = 2072919, upload-time = "2026-05-06T13:39:21.153Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ba/f463d006e0c47373ca7ec5e1a261c59dc01ef4d62b2657af925fb0deee3a/pydantic_core-2.46.4-cp312-cp312-win_arm64.whl", hash = "sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a", size = 2027604, upload-time = "2026-05-06T13:39:03.753Z" }, + { url = "https://files.pythonhosted.org/packages/51/a2/5d30b469c5267a17b39dec53208222f76a8d351dfac4af661888c5aee77d/pydantic_core-2.46.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008", size = 2106306, upload-time = "2026-05-06T13:37:48.029Z" }, + { url = "https://files.pythonhosted.org/packages/c1/81/4fa520eaffa8bd7d1525e644cd6d39e7d60b1592bc5b516693c7340b50f1/pydantic_core-2.46.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4", size = 1951906, upload-time = "2026-05-06T13:37:17.012Z" }, + { url = "https://files.pythonhosted.org/packages/03/d5/fd02da45b659668b05923b17ba3a0100a0a3d5541e3bd8fcc4ecb711309e/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76", size = 1976802, upload-time = "2026-05-06T13:37:35.113Z" }, + { url = "https://files.pythonhosted.org/packages/21/f2/95727e1368be3d3ed485eaab7adbd7dda408f33f7a36e8b48e0144002b91/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3", size = 2052446, upload-time = "2026-05-06T13:37:12.313Z" }, + { url = "https://files.pythonhosted.org/packages/9c/86/5d99feea3f77c7234b8718075b23db11532773c1a0dbd9b9490215dc2eeb/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76", size = 2232757, upload-time = "2026-05-06T13:39:01.149Z" }, + { url = "https://files.pythonhosted.org/packages/d2/3a/508ac615935ef7588cf6d9e9b91309fdc2da751af865e02a9098de88258c/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4", size = 2309275, upload-time = "2026-05-06T13:37:41.406Z" }, + { url = "https://files.pythonhosted.org/packages/07/f8/41db9de19d7987d6b04715a02b3b40aea467000275d9d758ffaa31af7d50/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a", size = 2094467, upload-time = "2026-05-06T13:39:18.847Z" }, + { url = "https://files.pythonhosted.org/packages/2c/e2/f35033184cb11d0052daf4416e8e10a502ea2ac006fc4f459aee872727d1/pydantic_core-2.46.4-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262", size = 2134417, upload-time = "2026-05-06T13:40:17.944Z" }, + { url = "https://files.pythonhosted.org/packages/7e/7b/6ceeb1cc90e193862f444ebe373d8fdf613f0a82572dde03fb10734c6c71/pydantic_core-2.46.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e", size = 2179782, upload-time = "2026-05-06T13:40:32.618Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f2/c8d7773ede6af08036423a00ae0ceffce266c3c52a096c435d68c896083f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd", size = 2188782, upload-time = "2026-05-06T13:36:51.018Z" }, + { url = "https://files.pythonhosted.org/packages/59/31/0c864784e31f09f05cdd87606f08923b9c9e7f6e51dd27f20f62f975ce9f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be", size = 2328334, upload-time = "2026-05-06T13:40:37.764Z" }, + { url = "https://files.pythonhosted.org/packages/c2/eb/4f6c8a41efa30baa755590f4141abf3a8c370fab610915733e74134a7270/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d", size = 2372986, upload-time = "2026-05-06T13:39:34.152Z" }, + { url = "https://files.pythonhosted.org/packages/5b/24/b375a480d53113860c299764bfe9f349a3dc9108b3adc0d7f0d786492ebf/pydantic_core-2.46.4-cp313-cp313-win32.whl", hash = "sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb", size = 1973693, upload-time = "2026-05-06T13:37:55.072Z" }, + { url = "https://files.pythonhosted.org/packages/7e/e8/cff247591966f2d22ec8c003cd7587e27b7ba7b81ab2fb888e3ab75dc285/pydantic_core-2.46.4-cp313-cp313-win_amd64.whl", hash = "sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292", size = 2071819, upload-time = "2026-05-06T13:38:49.139Z" }, + { url = "https://files.pythonhosted.org/packages/c6/1a/f4aee670d5670e9e148e0c82c7db98d780be566c6e6a97ee8035528ca0b3/pydantic_core-2.46.4-cp313-cp313-win_arm64.whl", hash = "sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d", size = 2027411, upload-time = "2026-05-06T13:40:45.796Z" }, + { url = "https://files.pythonhosted.org/packages/8d/74/228a26ddad29c6672b805d9fd78e8d251cd04004fa7eed0e622096cd0250/pydantic_core-2.46.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb", size = 2102079, upload-time = "2026-05-06T13:38:41.019Z" }, + { url = "https://files.pythonhosted.org/packages/ad/1f/8970b150a4b4365623ae00fc88603491f763c627311ae8031e3111356d6e/pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462", size = 1952179, upload-time = "2026-05-06T13:36:59.812Z" }, + { url = "https://files.pythonhosted.org/packages/95/30/5211a831ae054928054b2f79731661087a2bc5c01e825c672b3a4a8f1b3e/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9", size = 1978926, upload-time = "2026-05-06T13:37:39.933Z" }, + { url = "https://files.pythonhosted.org/packages/57/e9/689668733b1eb67adeef047db3c2e8788fcf65a7fd9c9e2b46b7744fe245/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4", size = 2046785, upload-time = "2026-05-06T13:38:01.995Z" }, + { url = "https://files.pythonhosted.org/packages/60/d9/6715260422ff50a2109878fd24d948a6c3446bb2664f34ee78cd972b3acd/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914", size = 2228733, upload-time = "2026-05-06T13:40:50.371Z" }, + { url = "https://files.pythonhosted.org/packages/18/ae/fdb2f64316afca925640f8e70bb1a564b0ec2721c1389e25b8eb4bf9a299/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28", size = 2307534, upload-time = "2026-05-06T13:37:21.531Z" }, + { url = "https://files.pythonhosted.org/packages/89/1d/8eff589b45bb8190a9d12c49cfad0f176a5cbd1534908a6b5125e2886239/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b", size = 2099732, upload-time = "2026-05-06T13:39:31.942Z" }, + { url = "https://files.pythonhosted.org/packages/06/d5/ee5a3366637fee41dee51a1fc91562dcf12ddbc68fda34e6b253da2324bb/pydantic_core-2.46.4-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c", size = 2129627, upload-time = "2026-05-06T13:37:25.033Z" }, + { url = "https://files.pythonhosted.org/packages/94/33/2414be571d2c6a6c4d08be21f9292b6d3fdb08949a97b6dfe985017821db/pydantic_core-2.46.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb", size = 2179141, upload-time = "2026-05-06T13:37:14.046Z" }, + { url = "https://files.pythonhosted.org/packages/7b/79/7daa95be995be0eecc4cf75064cb33f9bbbfe3fe0158caf2f0d4a996a5c7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898", size = 2184325, upload-time = "2026-05-06T13:36:53.615Z" }, + { url = "https://files.pythonhosted.org/packages/9f/cb/d0a382f5c0de8a222dc61c65348e0ce831b1f68e0a018450d31c2cace3a5/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e", size = 2323990, upload-time = "2026-05-06T13:40:29.971Z" }, + { url = "https://files.pythonhosted.org/packages/05/db/d9ba624cc4a5aced1598e88c04fdbd8310c8a69b9d38b9a3d39ce3a61ed7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519", size = 2369978, upload-time = "2026-05-06T13:37:23.027Z" }, + { url = "https://files.pythonhosted.org/packages/f2/20/d15df15ba918c423461905802bfd2981c3af0bfa0e40d05e13edbfa48bc3/pydantic_core-2.46.4-cp314-cp314-win32.whl", hash = "sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4", size = 1966354, upload-time = "2026-05-06T13:38:03.499Z" }, + { url = "https://files.pythonhosted.org/packages/fc/b6/6b8de4c0a7d7ab3004c439c80c5c1e0a3e8d78bbae19379b01960383d9e5/pydantic_core-2.46.4-cp314-cp314-win_amd64.whl", hash = "sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac", size = 2072238, upload-time = "2026-05-06T13:39:40.807Z" }, + { url = "https://files.pythonhosted.org/packages/32/36/51eb763beec1f4cf59b1db243a7dcc39cbb41230f050a09b9d69faaf0a48/pydantic_core-2.46.4-cp314-cp314-win_arm64.whl", hash = "sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a", size = 2018251, upload-time = "2026-05-06T13:37:26.72Z" }, + { url = "https://files.pythonhosted.org/packages/e8/91/855af51d625b23aa987116a19e231d2aaef9c4a415273ddc189b79a45fee/pydantic_core-2.46.4-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0", size = 2099593, upload-time = "2026-05-06T13:39:47.682Z" }, + { url = "https://files.pythonhosted.org/packages/fb/1b/8784a54c65edb5f49f0a14d6977cf1b209bba85a4c77445b255c2de58ab3/pydantic_core-2.46.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d", size = 1935226, upload-time = "2026-05-06T13:40:40.428Z" }, + { url = "https://files.pythonhosted.org/packages/e8/e7/1955d28d1afc56dd4b3ad7cc0cf39df1b9852964cf16e5d13912756d6d6b/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b", size = 1974605, upload-time = "2026-05-06T13:37:32.029Z" }, + { url = "https://files.pythonhosted.org/packages/93/e2/3fedbf0ba7a22850e6e9fd78117f1c0f10f950182344d8a6c535d468fdd8/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000", size = 2030777, upload-time = "2026-05-06T13:38:55.239Z" }, + { url = "https://files.pythonhosted.org/packages/f8/61/46be275fcaaba0b4f5b9669dd852267ce1ff616592dccf7a7845588df091/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e", size = 2236641, upload-time = "2026-05-06T13:37:08.096Z" }, + { url = "https://files.pythonhosted.org/packages/60/db/12e93e46a8bac9988be3c016860f83293daea8c716c029c9ace279036f2f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd", size = 2286404, upload-time = "2026-05-06T13:40:20.221Z" }, + { url = "https://files.pythonhosted.org/packages/e2/4a/4d8b19008f38d31c53b8219cfedc2e3d5de5fe99d90076b7e767de29274f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3", size = 2109219, upload-time = "2026-05-06T13:38:12.153Z" }, + { url = "https://files.pythonhosted.org/packages/88/70/3cbc40978fefb7bb09c6708d40d4ad1a5d70fd7213c3d17f971de868ec1f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7", size = 2110594, upload-time = "2026-05-06T13:40:02.971Z" }, + { url = "https://files.pythonhosted.org/packages/9d/20/b8d36736216e29491125531685b2f9e61aa5b4b2599893f8268551da3338/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff", size = 2159542, upload-time = "2026-05-06T13:39:27.506Z" }, + { url = "https://files.pythonhosted.org/packages/1d/a2/367df868eb584dacf6bf82a389272406d7178e301c4ac82545ab98bc2dd9/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424", size = 2168146, upload-time = "2026-05-06T13:38:31.93Z" }, + { url = "https://files.pythonhosted.org/packages/c1/b8/4460f77f7e201893f649a29ab355dddd3beee8a97bcb1a320db414f9a06e/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6", size = 2306309, upload-time = "2026-05-06T13:37:44.717Z" }, + { url = "https://files.pythonhosted.org/packages/64/c4/be2639293acd87dc8ddbcec41a73cee9b2ebf996fe6d892a1a74e88ad3f7/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565", size = 2369736, upload-time = "2026-05-06T13:37:05.645Z" }, + { url = "https://files.pythonhosted.org/packages/30/a6/9f9f380dbb301f67023bf8f707aaa75daadf84f7152d95c410fd7e81d994/pydantic_core-2.46.4-cp314-cp314t-win32.whl", hash = "sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02", size = 1955575, upload-time = "2026-05-06T13:38:51.116Z" }, + { url = "https://files.pythonhosted.org/packages/40/1f/f1eb9eb350e795d1af8586289746f5c5677d16043040d63710e22abc43c9/pydantic_core-2.46.4-cp314-cp314t-win_amd64.whl", hash = "sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5", size = 2051624, upload-time = "2026-05-06T13:38:21.672Z" }, + { url = "https://files.pythonhosted.org/packages/f6/d2/42dd53d0a85c27606f316d3aa5d2869c4e8470a5ed6dec30e4a1abe19192/pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596", size = 2017325, upload-time = "2026-05-06T13:40:52.723Z" }, + { url = "https://files.pythonhosted.org/packages/ee/a4/73995fd4ebbb46ba0ee51e6fa049b8f02c40daebb762208feda8a6b7894d/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:14d4edf427bdcf950a8a02d7cb44a08614388dd6e1bdcbf4f67504fa7887da9c", size = 2111589, upload-time = "2026-05-06T13:37:10.817Z" }, + { url = "https://files.pythonhosted.org/packages/fb/7f/f37d3a5e8bfcc2e403f5c57a730f2d815693fb42119e8ea48b3789335af1/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:0ce40cd7b21210e99342afafbd4d0f76d784eb5b1d60f3bdc566be4983c6c73b", size = 1944552, upload-time = "2026-05-06T13:36:56.717Z" }, + { url = "https://files.pythonhosted.org/packages/15/3c/d7eb777b3ff43e8433a4efb39a17aa8fd98a4ee8561a24a67ef5db07b2d6/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90884113d8b48f760e9587002789ddd741e76ab9f89518cd1e43b1f1a52ec44b", size = 1982984, upload-time = "2026-05-06T13:39:06.207Z" }, + { url = "https://files.pythonhosted.org/packages/63/87/70b9f40170a81afd55ca26c9b2acb25c20d64bcfbf888fafecb3ba077d4c/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66ce7632c22d837c95301830e111ad0128a32b8207533b60896a96c4915192ea", size = 2138417, upload-time = "2026-05-06T13:39:45.476Z" }, + { url = "https://files.pythonhosted.org/packages/9d/1d/8987ad40f65ae1432753072f214fb5c74fe47ffbd0698bb9cbbb585664f8/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7", size = 2095527, upload-time = "2026-05-06T13:39:52.283Z" }, + { url = "https://files.pythonhosted.org/packages/64/d3/84c282a7eee1d3ac4c0377546ef5a1ea436ce26840d9ac3b7ed54a377507/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df", size = 1936024, upload-time = "2026-05-06T13:40:15.671Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ca/eac61596cdeb4d7e174d3dc0bd8a6238f14f75f97a24e7b7db4c7e7340a0/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526", size = 1990696, upload-time = "2026-05-06T13:38:34.717Z" }, + { url = "https://files.pythonhosted.org/packages/fa/c3/7c8b240552251faf6b3a957db200fcfbbcec36763c050428b601e0c9b83b/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0", size = 2147590, upload-time = "2026-05-06T13:39:29.883Z" }, + { url = "https://files.pythonhosted.org/packages/11/cb/428de0385b6c8d44b716feba566abfacfbd23ee3c4439faa789a1456242f/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:0c563b08bca408dc7f65f700633d8442fffb2421fc47b8101377e9fd65051ff0", size = 2112782, upload-time = "2026-05-06T13:37:04.016Z" }, + { url = "https://files.pythonhosted.org/packages/0b/b5/6a17bdadd0fc1f170adfd05a20d37c832f52b117b4d9131da1f41bb097ce/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:db06ffe51636ffe9ca531fe9023dd64bdd794be8754cb5df57c5498ae5b518a7", size = 1952146, upload-time = "2026-05-06T13:39:43.092Z" }, + { url = "https://files.pythonhosted.org/packages/2a/dc/03734d80e362cd43ef65428e9de77c730ce7f2f11c60d2b1e1b39f0fbf99/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:133878133d271ade3d41d1bfb2a45ec38dbdbda40bc065921c6b04e4630127e2", size = 2134492, upload-time = "2026-05-06T13:36:58.124Z" }, + { url = "https://files.pythonhosted.org/packages/de/df/5e5ffc085ed07cc22d298134d3d911c63e91f6a0eb91fe646750a3209910/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9bc519fbf2b7578398853d815009ae5e4d4603d12f4e3f91da8c06852d3da3e9", size = 2156604, upload-time = "2026-05-06T13:37:49.88Z" }, + { url = "https://files.pythonhosted.org/packages/81/44/6e112a4253e56f5705467cbab7ab5e91ee7398ba3d56d358635958893d3e/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c7a7bd4e39e8e4c12c39cd480356842b6a8a06e41b23a55a5e3e191718838ddf", size = 2183828, upload-time = "2026-05-06T13:37:43.053Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ad/5565071e937d8e752842ac241463944c9eb14c87e2d269f2658a5bd05e98/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:d396ec2b979760aaf3218e76c24e65bd0aca24983298653b3a9d7a45f9e47b30", size = 2310000, upload-time = "2026-05-06T13:37:56.694Z" }, + { url = "https://files.pythonhosted.org/packages/4f/c3/66883a5cec183e7fba4d024b4cbbe61851a63750ef606b0afecc46d1f2bf/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:86e1a4418c6cd97d60c95c71164158eaf7324fae7b0923264016baa993eba6fc", size = 2361286, upload-time = "2026-05-06T13:40:05.667Z" }, + { url = "https://files.pythonhosted.org/packages/4b/2d/69abac8f838090bbecd5df894befb2c2619e7996a98ddb949db9f3b93225/pydantic_core-2.46.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:d51026d73fcfd93610abc7b27789c26b313920fcfb20e27462d74a7f8b06e983", size = 2193071, upload-time = "2026-05-06T13:38:08.682Z" }, ] [[package]] @@ -2386,7 +2365,7 @@ wheels = [ [[package]] name = "pydocket" -version = "0.20.0" +version = "0.20.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "burner-redis" }, @@ -2405,9 +2384,9 @@ dependencies = [ { name = "tzdata", marker = "sys_platform == 'win32'" }, { name = "uncalled-for" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/bc/9d/05d54dccfaa505c0bc2e480bc331c44552cdc16af3f44f5893c75293a165/pydocket-0.20.0.tar.gz", hash = "sha256:4b5132a5754ba54f894d46bf2cbdc12e237adada73bc76ca367017536098df7f", size = 361050, upload-time = "2026-05-04T00:27:34.393Z" } +sdist = { url = "https://files.pythonhosted.org/packages/72/bf/7f1134e990855f373e5ee6ba316db8fe654a2d7dd852b41ab890fcfb91e3/pydocket-0.20.1.tar.gz", hash = "sha256:d72b3784e4b5069b39e5f49f599d54a891e1b6222c27a8bcfbd4dee0f57d4895", size = 361993, upload-time = "2026-05-06T14:06:25.956Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/17/cb/635665c07be980ec48c92b830907e6796012801b107cb1166a213e49ec38/pydocket-0.20.0-py3-none-any.whl", hash = "sha256:1f745278be09d3526f1bdd579c2d92f77fa0a534a39b893e9ef21dfc2ee52378", size = 102483, upload-time = "2026-05-04T00:27:32.799Z" }, + { url = "https://files.pythonhosted.org/packages/9d/9d/1bd873a0ea480dec388c40ac1a7500c129efbb9d61e2fef6b97236703458/pydocket-0.20.1-py3-none-any.whl", hash = "sha256:c886ece90ac93018f069d1eef9443f888404081d7258955e16847752575c95ae", size = 102774, upload-time = "2026-05-06T14:06:24.548Z" }, ] [[package]] @@ -2503,11 +2482,11 @@ wheels = [ [[package]] name = "pyreadline3" -version = "3.5.4" +version = "3.5.6" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0f/49/4cea918a08f02817aabae639e3d0ac046fef9f9180518a3ad394e22da148/pyreadline3-3.5.4.tar.gz", hash = "sha256:8d57d53039a1c75adba8e50dd3d992b28143480816187ea5efbd5c78e6c885b7", size = 99839, upload-time = "2024-09-19T02:40:10.062Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b6/6d/f94028646d7bbe6d9d873c47ee7c246f2d29129d253f0d96cb6fcab70733/pyreadline3-3.5.6.tar.gz", hash = "sha256:61e53218b99656091ddb077df9e71f25850e72e030b6183b39c9b7e6e4f4a9bf", size = 100368, upload-time = "2026-05-14T17:55:04.471Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5a/dc/491b7661614ab97483abf2056be1deee4dc2490ecbf7bff9ab5cdbac86e1/pyreadline3-3.5.4-py3-none-any.whl", hash = "sha256:eaf8e6cc3c49bcccf145fc6067ba8643d1df34d604a1ec0eccbf7a18e6d3fae6", size = 83178, upload-time = "2024-09-19T02:40:08.598Z" }, + { url = "https://files.pythonhosted.org/packages/f7/5e/35c856e186b74678c24927847ad9895a51f1bc02a0c6126477a6c6040064/pyreadline3-3.5.6-py3-none-any.whl", hash = "sha256:8449b734232e42a5dcd74048e39b60db2839a4c38cf3ae2bf7707d58b5389c0d", size = 85243, upload-time = "2026-05-14T17:55:03.262Z" }, ] [[package]] @@ -2521,7 +2500,7 @@ wheels = [ [[package]] name = "pytest" -version = "9.0.3" +version = "9.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, @@ -2532,23 +2511,23 @@ dependencies = [ { name = "pygments" }, { name = "tomli", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" }, + { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" }, ] [[package]] name = "pytest-asyncio" -version = "1.3.0" +version = "1.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "backports-asyncio-runner", marker = "python_full_version < '3.11'" }, { name = "pytest" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/90/2c/8af215c0f776415f3590cac4f9086ccefd6fd463befeae41cd4d3f193e5a/pytest_asyncio-1.3.0.tar.gz", hash = "sha256:d7f52f36d231b80ee124cd216ffb19369aa168fc10095013c6b014a34d3ee9e5", size = 50087, upload-time = "2025-11-10T16:07:47.256Z" } +sdist = { url = "https://files.pythonhosted.org/packages/43/7c/d36d04db312ecf4298932ef77e6e4a9e8ad017906e24e34f0b0c361a2473/pytest_asyncio-1.4.0.tar.gz", hash = "sha256:c6c0d2259945122819f171a32ecea2c349ead889ee28176caaf492143424be42", size = 58514, upload-time = "2026-05-26T09:56:04.083Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl", hash = "sha256:611e26147c7f77640e6d0a92a38ed17c3e9848063698d5c93d5aa7aa11cebff5", size = 15075, upload-time = "2025-11-10T16:07:45.537Z" }, + { url = "https://files.pythonhosted.org/packages/03/e2/08a497ef684b88559c9cc5f4ad53a37e7b99e727094a86d6ea32536d5d3c/pytest_asyncio-1.4.0-py3-none-any.whl", hash = "sha256:933ca923a23075a87fb7070c0ec272a6848489824d887c85c812670932835aa1", size = 16930, upload-time = "2026-05-26T09:56:02.576Z" }, ] [[package]] @@ -2567,16 +2546,16 @@ wheels = [ [[package]] name = "pytest-env" -version = "1.6.0" +version = "1.7.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pytest" }, { name = "python-dotenv" }, { name = "tomli", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ff/69/4db1c30625af0621df8dbe73797b38b6d1b04e15d021dd5d26a6d297f78c/pytest_env-1.6.0.tar.gz", hash = "sha256:ac02d6fba16af54d61e311dd70a3c61024a4e966881ea844affc3c8f0bf207d3", size = 16163, upload-time = "2026-03-12T22:39:43.78Z" } +sdist = { url = "https://files.pythonhosted.org/packages/65/49/08ee056f9cc655e437abcf2ae399884844b623223476ae6a77244131db03/pytest_env-1.7.0.tar.gz", hash = "sha256:0c1dc1101fb8d3ab3611e8f8d657ba06c3c0c167fc85c90457e5b27f2508f43e", size = 16408, upload-time = "2026-07-21T13:09:21.834Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/27/16/ad52f56b96d851a2bcfdc1e754c3531341885bd7177a128c13ff2ca72ab4/pytest_env-1.6.0-py3-none-any.whl", hash = "sha256:1e7f8a62215e5885835daaed694de8657c908505b964ec8097a7ce77b403d9a3", size = 10400, upload-time = "2026-03-12T22:39:41.887Z" }, + { url = "https://files.pythonhosted.org/packages/3e/fc/9f2975c41d41bf5bd9a7d0fc03085ec20052b456b079df53828ae4a1b100/pytest_env-1.7.0-py3-none-any.whl", hash = "sha256:9ee0f1fe859d23fcdb533fe2909a404b3b133d02674a56df275bbe4df4eb104b", size = 10263, upload-time = "2026-07-21T13:09:20.677Z" }, ] [[package]] @@ -2719,24 +2698,27 @@ wheels = [ [[package]] name = "pywin32" -version = "311" +version = "312" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7b/40/44efbb0dfbd33aca6a6483191dae0716070ed99e2ecb0c53683f400a0b4f/pywin32-311-cp310-cp310-win32.whl", hash = "sha256:d03ff496d2a0cd4a5893504789d4a15399133fe82517455e78bad62efbb7f0a3", size = 8760432, upload-time = "2025-07-14T20:13:05.9Z" }, - { url = "https://files.pythonhosted.org/packages/5e/bf/360243b1e953bd254a82f12653974be395ba880e7ec23e3731d9f73921cc/pywin32-311-cp310-cp310-win_amd64.whl", hash = "sha256:797c2772017851984b97180b0bebe4b620bb86328e8a884bb626156295a63b3b", size = 9590103, upload-time = "2025-07-14T20:13:07.698Z" }, - { url = "https://files.pythonhosted.org/packages/57/38/d290720e6f138086fb3d5ffe0b6caa019a791dd57866940c82e4eeaf2012/pywin32-311-cp310-cp310-win_arm64.whl", hash = "sha256:0502d1facf1fed4839a9a51ccbcc63d952cf318f78ffc00a7e78528ac27d7a2b", size = 8778557, upload-time = "2025-07-14T20:13:11.11Z" }, - { url = "https://files.pythonhosted.org/packages/7c/af/449a6a91e5d6db51420875c54f6aff7c97a86a3b13a0b4f1a5c13b988de3/pywin32-311-cp311-cp311-win32.whl", hash = "sha256:184eb5e436dea364dcd3d2316d577d625c0351bf237c4e9a5fabbcfa5a58b151", size = 8697031, upload-time = "2025-07-14T20:13:13.266Z" }, - { url = "https://files.pythonhosted.org/packages/51/8f/9bb81dd5bb77d22243d33c8397f09377056d5c687aa6d4042bea7fbf8364/pywin32-311-cp311-cp311-win_amd64.whl", hash = "sha256:3ce80b34b22b17ccbd937a6e78e7225d80c52f5ab9940fe0506a1a16f3dab503", size = 9508308, upload-time = "2025-07-14T20:13:15.147Z" }, - { url = "https://files.pythonhosted.org/packages/44/7b/9c2ab54f74a138c491aba1b1cd0795ba61f144c711daea84a88b63dc0f6c/pywin32-311-cp311-cp311-win_arm64.whl", hash = "sha256:a733f1388e1a842abb67ffa8e7aad0e70ac519e09b0f6a784e65a136ec7cefd2", size = 8703930, upload-time = "2025-07-14T20:13:16.945Z" }, - { url = "https://files.pythonhosted.org/packages/e7/ab/01ea1943d4eba0f850c3c61e78e8dd59757ff815ff3ccd0a84de5f541f42/pywin32-311-cp312-cp312-win32.whl", hash = "sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31", size = 8706543, upload-time = "2025-07-14T20:13:20.765Z" }, - { url = "https://files.pythonhosted.org/packages/d1/a8/a0e8d07d4d051ec7502cd58b291ec98dcc0c3fff027caad0470b72cfcc2f/pywin32-311-cp312-cp312-win_amd64.whl", hash = "sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067", size = 9495040, upload-time = "2025-07-14T20:13:22.543Z" }, - { url = "https://files.pythonhosted.org/packages/ba/3a/2ae996277b4b50f17d61f0603efd8253cb2d79cc7ae159468007b586396d/pywin32-311-cp312-cp312-win_arm64.whl", hash = "sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852", size = 8710102, upload-time = "2025-07-14T20:13:24.682Z" }, - { url = "https://files.pythonhosted.org/packages/a5/be/3fd5de0979fcb3994bfee0d65ed8ca9506a8a1260651b86174f6a86f52b3/pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d", size = 8705700, upload-time = "2025-07-14T20:13:26.471Z" }, - { url = "https://files.pythonhosted.org/packages/e3/28/e0a1909523c6890208295a29e05c2adb2126364e289826c0a8bc7297bd5c/pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d", size = 9494700, upload-time = "2025-07-14T20:13:28.243Z" }, - { url = "https://files.pythonhosted.org/packages/04/bf/90339ac0f55726dce7d794e6d79a18a91265bdf3aa70b6b9ca52f35e022a/pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a", size = 8709318, upload-time = "2025-07-14T20:13:30.348Z" }, - { url = "https://files.pythonhosted.org/packages/c9/31/097f2e132c4f16d99a22bfb777e0fd88bd8e1c634304e102f313af69ace5/pywin32-311-cp314-cp314-win32.whl", hash = "sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee", size = 8840714, upload-time = "2025-07-14T20:13:32.449Z" }, - { url = "https://files.pythonhosted.org/packages/90/4b/07c77d8ba0e01349358082713400435347df8426208171ce297da32c313d/pywin32-311-cp314-cp314-win_amd64.whl", hash = "sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87", size = 9656800, upload-time = "2025-07-14T20:13:34.312Z" }, - { url = "https://files.pythonhosted.org/packages/c0/d2/21af5c535501a7233e734b8af901574572da66fcc254cb35d0609c9080dd/pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42", size = 8932540, upload-time = "2025-07-14T20:13:36.379Z" }, + { url = "https://files.pythonhosted.org/packages/fe/1b/9cfdeac80ee45bebbbcb31f1b7b99a0d81a1c72de48d837be984e0e88b1d/pywin32-312-cp310-cp310-win32.whl", hash = "sha256:772235332b5d1024c696f11cea1ae4be7930f0a8b894bb43db14e3f435f1ff7e", size = 6361387, upload-time = "2026-06-04T07:49:14.329Z" }, + { url = "https://files.pythonhosted.org/packages/33/b1/7afc96d041d982c27bc2df6f853d43f01fd273e3d39d04be3647ddeb533d/pywin32-312-cp310-cp310-win_amd64.whl", hash = "sha256:5dbc35d2b5320dc07f25fa31269cfb767471002b17de5eb067d03da68c7cb2db", size = 6926780, upload-time = "2026-06-04T07:49:16.881Z" }, + { url = "https://files.pythonhosted.org/packages/ce/3a/4140da9ad54108e517f4a16b2d83da3033e08662144623e1239587cb7db6/pywin32-312-cp310-cp310-win_arm64.whl", hash = "sha256:3020656e34f1cf7faeb7bccd2b84653a607c6ff0c55ada85e6487d61716deabd", size = 4307203, upload-time = "2026-06-04T07:49:18.993Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f5/10a6e845a00fc5e7afd0a988b744f403d4d57162a28d160a093c4d9322f0/pywin32-312-cp311-cp311-win32.whl", hash = "sha256:17948aeadbdb091f0ced6ef0841620794e68327b94ee415571c1203594b7215c", size = 6362659, upload-time = "2026-06-04T07:49:21.349Z" }, + { url = "https://files.pythonhosted.org/packages/35/c4/dcd2d62b5944b6d5db53413a5899016ccd57ffcb7278f3f81655d25d2027/pywin32-312-cp311-cp311-win_amd64.whl", hash = "sha256:d11417d84412f859b722fad0841b3614459ed0047f7542d8362e77884f6b6e8a", size = 6928825, upload-time = "2026-06-04T07:49:23.934Z" }, + { url = "https://files.pythonhosted.org/packages/b7/56/3cbb433fe4501cdba2eb9040f56a4e1a8243faa4186b25295564d1a7a79d/pywin32-312-cp311-cp311-win_arm64.whl", hash = "sha256:b2200a054ca6d6625c4842fc56a4976a4b47f96b73dbe5538c3f813a80359f47", size = 6721875, upload-time = "2026-06-04T07:49:26.416Z" }, + { url = "https://files.pythonhosted.org/packages/83/ff/32aa7d2ed0ab12b323aaa64f9b75e6ad4f8fd09f9ccfc28c79414d46838d/pywin32-312-cp312-cp312-win32.whl", hash = "sha256:dab4f65ac9c4e48400a2a0530c46c3c579cd5905ecd11b80692373915269208b", size = 6371877, upload-time = "2026-06-04T07:49:28.836Z" }, + { url = "https://files.pythonhosted.org/packages/03/d9/77040d3b43df3f3be32ea289433d660d2727f5ba327bc73be835127d9d60/pywin32-312-cp312-cp312-win_amd64.whl", hash = "sha256:b457f6d628a47e8a7346ce22acb7e1a46a4a78b52e1d17e1af56871bd19a93bc", size = 6914841, upload-time = "2026-06-04T07:49:31.85Z" }, + { url = "https://files.pythonhosted.org/packages/e3/cc/7b1ec671775756020a0ee7f4feeaf3c568f0ab86bd3900088cf986937a92/pywin32-312-cp312-cp312-win_arm64.whl", hash = "sha256:6017c58e12f6809fbb0555b75df144c2922a9ffd18e4b9b5afa863b6c1a9d950", size = 6727901, upload-time = "2026-06-04T07:49:34.244Z" }, + { url = "https://files.pythonhosted.org/packages/2d/41/12fbfd7f36ed2146d8bc9de96c2741296bf0d490b98508496cff322e274c/pywin32-312-cp313-cp313-win32.whl", hash = "sha256:7a27df850933d16a8eabfbaeb73d52b273e2da667f80d70b01a89d1f6828d02c", size = 6370184, upload-time = "2026-06-04T07:49:36.253Z" }, + { url = "https://files.pythonhosted.org/packages/ba/db/36a78e3403099d31d9746d13fdcde5accc43c1155f375a34d15983a479a7/pywin32-312-cp313-cp313-win_amd64.whl", hash = "sha256:c53e878d15a1c44788082bfe712a905433473aa38f86375b7cf8b45e3acbaaf9", size = 6914298, upload-time = "2026-06-04T07:49:38.876Z" }, + { url = "https://files.pythonhosted.org/packages/84/37/c1697194092b76de9ed47ca124323f02c57ffc8a45c06f88a3d5acaf01eb/pywin32-312-cp313-cp313-win_arm64.whl", hash = "sha256:59aba5d5940842075343a5ddc6b11f1cdf0d1567fe745290359dfbcc7c2eb831", size = 6727640, upload-time = "2026-06-04T07:49:41.083Z" }, + { url = "https://files.pythonhosted.org/packages/fc/2b/1f3cded5822fd49c02f40544cbb5f58c7cfd6b1694869fd476cb6170ee97/pywin32-312-cp314-cp314-win32.whl", hash = "sha256:a77a90fbb6881238d2ca9c6fd797b25817f3768fe78d214a90137ff055a75f5b", size = 6468928, upload-time = "2026-06-04T07:49:43.188Z" }, + { url = "https://files.pythonhosted.org/packages/21/82/3bf86d2e2808902013132e1ce905a7da0da53790f3836c64bf44d55e24f3/pywin32-312-cp314-cp314-win_amd64.whl", hash = "sha256:a4dd3a848290ef724347b19f301045831d8e802fa4464f491b98b1e0a081432e", size = 7024157, upload-time = "2026-06-04T07:49:45.34Z" }, + { url = "https://files.pythonhosted.org/packages/a4/0e/73f6d6800b4f27655abd9e9f6aaeaefcddb2b946e4674efa2bab184a7f7b/pywin32-312-cp314-cp314-win_arm64.whl", hash = "sha256:9fce94568364e0155e6dfb781ac5d95903be8baf28670632beab1b523f300daa", size = 6839598, upload-time = "2026-06-04T07:49:47.613Z" }, + { url = "https://files.pythonhosted.org/packages/eb/61/caa39686032d2ebdd04ff0ab5cbe163126c0066d98e00c9018646e42393b/pywin32-312-cp315-cp315-win32.whl", hash = "sha256:5c1fbe4a937a73ae9297384a3da38518cbc694c68ad8a809b2e19acd350f03ed", size = 6471159, upload-time = "2026-06-04T07:49:50.035Z" }, + { url = "https://files.pythonhosted.org/packages/0f/cd/7e1de64a4a6f69c04214169657ccab0d93a670ea50e35eb8f489d7378249/pywin32-312-cp315-cp315-win_amd64.whl", hash = "sha256:c2f03a0f73f804a13c2735b99392b0cd426bb4f2c4d0178e5ac966a0f21618d5", size = 7025293, upload-time = "2026-06-04T07:49:54.857Z" }, + { url = "https://files.pythonhosted.org/packages/23/ed/4532e9388e65fa16b46776ef47ad631a64eda1631884488af707666350ed/pywin32-312-cp315-cp315-win_arm64.whl", hash = "sha256:a8597d28f267b39074aef51fa593530082b39cbe5a074226096857b1fed2dfb9", size = 6840337, upload-time = "2026-06-04T07:49:57.531Z" }, ] [[package]] @@ -2814,14 +2796,14 @@ wheels = [ [[package]] name = "redis" -version = "7.4.0" +version = "8.0.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "async-timeout", marker = "python_full_version < '3.11.3'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7b/7f/3759b1d0d72b7c92f0d70ffd9dc962b7b7b5ee74e135f9d7d8ab06b8a318/redis-7.4.0.tar.gz", hash = "sha256:64a6ea7bf567ad43c964d2c30d82853f8df927c5c9017766c55a1d1ed95d18ad", size = 4943913, upload-time = "2026-03-24T09:14:37.53Z" } +sdist = { url = "https://files.pythonhosted.org/packages/cc/c3/928b290c2c0ca99ab96eea5b4ff8f30be8112b075301a7d3ba214a3c8c12/redis-8.0.1.tar.gz", hash = "sha256:afc5a7a2f5a084f5b1880dec548dd45be17db7e43c82a30d84f952aefb05cfb0", size = 5114170, upload-time = "2026-06-23T14:52:37.728Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/74/3a/95deec7db1eb53979973ebd156f3369a72732208d1391cd2e5d127062a32/redis-7.4.0-py3-none-any.whl", hash = "sha256:a9c74a5c893a5ef8455a5adb793a31bb70feb821c86eccb62eebef5a19c429ec", size = 409772, upload-time = "2026-03-24T09:14:35.968Z" }, + { url = "https://files.pythonhosted.org/packages/fd/0a/c2345ebf1ebe70840ce3f6c6ee612f8fa749cfbd1b03069c53bf0c62aaad/redis-8.0.1-py3-none-any.whl", hash = "sha256:47daa35a058c23468d6437f17a8c76882cb316b838ef763036af99b96cedd743", size = 502406, upload-time = "2026-06-23T14:52:36.137Z" }, ] [[package]] @@ -2830,7 +2812,8 @@ version = "0.37.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "attrs" }, - { name = "rpds-py" }, + { name = "rpds-py", version = "0.30.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "rpds-py", version = "2026.6.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" } @@ -2840,7 +2823,7 @@ wheels = [ [[package]] name = "requests" -version = "2.33.0" +version = "2.34.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, @@ -2848,41 +2831,44 @@ dependencies = [ { name = "idna" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/34/64/8860370b167a9721e8956ae116825caff829224fbca0ca6e7bf8ddef8430/requests-2.33.0.tar.gz", hash = "sha256:c7ebc5e8b0f21837386ad0e1c8fe8b829fa5f544d8df3b2253bff14ef29d7652", size = 134232, upload-time = "2026-03-25T15:10:41.586Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/56/5d/c814546c2333ceea4ba42262d8c4d55763003e767fa169adc693bd524478/requests-2.33.0-py3-none-any.whl", hash = "sha256:3324635456fa185245e24865e810cecec7b4caf933d7eb133dcde67d48cee69b", size = 65017, upload-time = "2026-03-25T15:10:40.382Z" }, + { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" }, ] [[package]] name = "rich" -version = "14.3.3" +version = "15.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown-it-py" }, { name = "pygments" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b3/c6/f3b320c27991c46f43ee9d856302c70dc2d0fb2dba4842ff739d5f46b393/rich-14.3.3.tar.gz", hash = "sha256:b8daa0b9e4eef54dd8cf7c86c03713f53241884e814f4e2f5fb342fe520f639b", size = 230582, upload-time = "2026-02-19T17:23:12.474Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/14/25/b208c5683343959b670dc001595f2f3737e051da617f66c31f7c4fa93abc/rich-14.3.3-py3-none-any.whl", hash = "sha256:793431c1f8619afa7d3b52b2cdec859562b950ea0d4b6b505397612db8d5362d", size = 310458, upload-time = "2026-02-19T17:23:13.732Z" }, + { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" }, ] [[package]] name = "rich-rst" -version = "1.3.2" +version = "2.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "docutils" }, + { name = "pygments" }, { name = "rich" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/bc/6d/a506aaa4a9eaa945ed8ab2b7347859f53593864289853c5d6d62b77246e0/rich_rst-1.3.2.tar.gz", hash = "sha256:a1196fdddf1e364b02ec68a05e8ff8f6914fee10fbca2e6b6735f166bb0da8d4", size = 14936, upload-time = "2025-10-14T16:49:45.332Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e2/d6/d0b9fafc73b65767200da027acab1db1bdb1048f4fea5ebf659df01c700e/rich_rst-2.1.0.tar.gz", hash = "sha256:f4d117b49697f338769759fa5cacf5197da4888b347b9fda2e50aef5cd8d93bd", size = 302732, upload-time = "2026-07-05T02:59:44.308Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/13/2f/b4530fbf948867702d0a3f27de4a6aab1d156f406d72852ab902c4d04de9/rich_rst-1.3.2-py3-none-any.whl", hash = "sha256:a99b4907cbe118cf9d18b0b44de272efa61f15117c61e39ebdc431baf5df722a", size = 12567, upload-time = "2025-10-14T16:49:42.953Z" }, + { url = "https://files.pythonhosted.org/packages/2a/68/1fc93dd759605b5d00fc98b50200739e41ed32bd22d6ba35ca6c3932371b/rich_rst-2.1.0-py3-none-any.whl", hash = "sha256:7ecd1343ee12c879d0e7ae74c3eb6d263b023d2929c6d114212eb1fd91057255", size = 272987, upload-time = "2026-07-05T02:59:42.792Z" }, ] [[package]] name = "rpds-py" version = "0.30.0" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11'", +] sdist = { url = "https://files.pythonhosted.org/packages/20/af/3f2f423103f1113b36230496629986e0ef7e199d2aa8392452b484b38ced/rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84", size = 69469, upload-time = "2025-11-30T20:24:38.837Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/06/0c/0c411a0ec64ccb6d104dcabe0e713e05e153a9a2c3c2bd2b32ce412166fe/rpds_py-0.30.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:679ae98e00c0e8d68a7fda324e16b90fd5260945b45d3b824c892cec9eea3288", size = 370490, upload-time = "2025-11-30T20:21:33.256Z" }, @@ -3002,28 +2988,156 @@ wheels = [ ] [[package]] -name = "ruff" -version = "0.15.8" +name = "rpds-py" +version = "2026.6.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/14/b0/73cf7550861e2b4824950b8b52eebdcc5adc792a00c514406556c5b80817/ruff-0.15.8.tar.gz", hash = "sha256:995f11f63597ee362130d1d5a327a87cb6f3f5eae3094c620bcc632329a4d26e", size = 4610921, upload-time = "2026-03-26T18:39:38.675Z" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version == '3.13.*'", + "python_full_version >= '3.11' and python_full_version < '3.13'", +] +sdist = { url = "https://files.pythonhosted.org/packages/aa/2a/9618a122aeb2a169a28b03889a2995fe297588964333d4a7d67bdf46e147/rpds_py-2026.6.3.tar.gz", hash = "sha256:1cebd1337c242e4ec2293e541f712b2da849b29f48f0c293684b71c0632625d4", size = 64051, upload-time = "2026-06-30T07:17:53.009Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4a/92/c445b0cd6da6e7ae51e954939cb69f97e008dbe750cfca89b8cedc081be7/ruff-0.15.8-py3-none-linux_armv6l.whl", hash = "sha256:cbe05adeba76d58162762d6b239c9056f1a15a55bd4b346cfd21e26cd6ad7bc7", size = 10527394, upload-time = "2026-03-26T18:39:41.566Z" }, - { url = "https://files.pythonhosted.org/packages/eb/92/f1c662784d149ad1414cae450b082cf736430c12ca78367f20f5ed569d65/ruff-0.15.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:d3e3d0b6ba8dca1b7ef9ab80a28e840a20070c4b62e56d675c24f366ef330570", size = 10905693, upload-time = "2026-03-26T18:39:30.364Z" }, - { url = "https://files.pythonhosted.org/packages/ca/f2/7a631a8af6d88bcef997eb1bf87cc3da158294c57044aafd3e17030613de/ruff-0.15.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:6ee3ae5c65a42f273f126686353f2e08ff29927b7b7e203b711514370d500de3", size = 10323044, upload-time = "2026-03-26T18:39:33.37Z" }, - { url = "https://files.pythonhosted.org/packages/67/18/1bf38e20914a05e72ef3b9569b1d5c70a7ef26cd188d69e9ca8ef588d5bf/ruff-0.15.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdce027ada77baa448077ccc6ebb2fa9c3c62fd110d8659d601cf2f475858d94", size = 10629135, upload-time = "2026-03-26T18:39:44.142Z" }, - { url = "https://files.pythonhosted.org/packages/d2/e9/138c150ff9af60556121623d41aba18b7b57d95ac032e177b6a53789d279/ruff-0.15.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:12e617fc01a95e5821648a6df341d80456bd627bfab8a829f7cfc26a14a4b4a3", size = 10348041, upload-time = "2026-03-26T18:39:52.178Z" }, - { url = "https://files.pythonhosted.org/packages/02/f1/5bfb9298d9c323f842c5ddeb85f1f10ef51516ac7a34ba446c9347d898df/ruff-0.15.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:432701303b26416d22ba696c39f2c6f12499b89093b61360abc34bcc9bf07762", size = 11121987, upload-time = "2026-03-26T18:39:55.195Z" }, - { url = "https://files.pythonhosted.org/packages/10/11/6da2e538704e753c04e8d86b1fc55712fdbdcc266af1a1ece7a51fff0d10/ruff-0.15.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d910ae974b7a06a33a057cb87d2a10792a3b2b3b35e33d2699fdf63ec8f6b17a", size = 11951057, upload-time = "2026-03-26T18:39:19.18Z" }, - { url = "https://files.pythonhosted.org/packages/83/f0/c9208c5fd5101bf87002fed774ff25a96eea313d305f1e5d5744698dc314/ruff-0.15.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2033f963c43949d51e6fdccd3946633c6b37c484f5f98c3035f49c27395a8ab8", size = 11464613, upload-time = "2026-03-26T18:40:06.301Z" }, - { url = "https://files.pythonhosted.org/packages/f8/22/d7f2fabdba4fae9f3b570e5605d5eb4500dcb7b770d3217dca4428484b17/ruff-0.15.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f29b989a55572fb885b77464cf24af05500806ab4edf9a0fd8977f9759d85b1", size = 11257557, upload-time = "2026-03-26T18:39:57.972Z" }, - { url = "https://files.pythonhosted.org/packages/71/8c/382a9620038cf6906446b23ce8632ab8c0811b8f9d3e764f58bedd0c9a6f/ruff-0.15.8-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:ac51d486bf457cdc985a412fb1801b2dfd1bd8838372fc55de64b1510eff4bec", size = 11169440, upload-time = "2026-03-26T18:39:22.205Z" }, - { url = "https://files.pythonhosted.org/packages/4d/0d/0994c802a7eaaf99380085e4e40c845f8e32a562e20a38ec06174b52ef24/ruff-0.15.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:c9861eb959edab053c10ad62c278835ee69ca527b6dcd72b47d5c1e5648964f6", size = 10605963, upload-time = "2026-03-26T18:39:46.682Z" }, - { url = "https://files.pythonhosted.org/packages/19/aa/d624b86f5b0aad7cef6bbf9cd47a6a02dfdc4f72c92a337d724e39c9d14b/ruff-0.15.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8d9a5b8ea13f26ae90838afc33f91b547e61b794865374f114f349e9036835fb", size = 10357484, upload-time = "2026-03-26T18:39:49.176Z" }, - { url = "https://files.pythonhosted.org/packages/35/c3/e0b7835d23001f7d999f3895c6b569927c4d39912286897f625736e1fd04/ruff-0.15.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c2a33a529fb3cbc23a7124b5c6ff121e4d6228029cba374777bd7649cc8598b8", size = 10830426, upload-time = "2026-03-26T18:40:03.702Z" }, - { url = "https://files.pythonhosted.org/packages/f0/51/ab20b322f637b369383adc341d761eaaa0f0203d6b9a7421cd6e783d81b9/ruff-0.15.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:75e5cd06b1cf3f47a3996cfc999226b19aa92e7cce682dcd62f80d7035f98f49", size = 11345125, upload-time = "2026-03-26T18:39:27.799Z" }, - { url = "https://files.pythonhosted.org/packages/37/e6/90b2b33419f59d0f2c4c8a48a4b74b460709a557e8e0064cf33ad894f983/ruff-0.15.8-py3-none-win32.whl", hash = "sha256:bc1f0a51254ba21767bfa9a8b5013ca8149dcf38092e6a9eb704d876de94dc34", size = 10571959, upload-time = "2026-03-26T18:39:36.117Z" }, - { url = "https://files.pythonhosted.org/packages/1f/a2/ef467cb77099062317154c63f234b8a7baf7cb690b99af760c5b68b9ee7f/ruff-0.15.8-py3-none-win_amd64.whl", hash = "sha256:04f79eff02a72db209d47d665ba7ebcad609d8918a134f86cb13dd132159fc89", size = 11743893, upload-time = "2026-03-26T18:39:25.01Z" }, - { url = "https://files.pythonhosted.org/packages/15/e2/77be4fff062fa78d9b2a4dea85d14785dac5f1d0c1fb58ed52331f0ebe28/ruff-0.15.8-py3-none-win_arm64.whl", hash = "sha256:cf891fa8e3bb430c0e7fac93851a5978fc99c8fa2c053b57b118972866f8e5f2", size = 11048175, upload-time = "2026-03-26T18:40:01.06Z" }, + { url = "https://files.pythonhosted.org/packages/94/1f/a2dca5ffdbf1d475ffc4e80e4d5d720ff3a00f691795910116960ee12511/rpds_py-2026.6.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:7b689145a1485c335569bd056464f3243a29af7ed3871c7be31ad624ba239bc7", size = 342174, upload-time = "2026-06-30T07:14:54.821Z" }, + { url = "https://files.pythonhosted.org/packages/4d/dc/323d08583c0832911768663d1944f0107fcd4088704858d84b5e06d105a0/rpds_py-2026.6.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:db08f45aecde626498fb3df07bcf6d2ec040af42e859a4f5040d79c200342911", size = 345513, upload-time = "2026-06-30T07:14:56.515Z" }, + { url = "https://files.pythonhosted.org/packages/0b/2a/e31989834d18d2f26ec1d2774c5b1eb3331df4ea8ada525175294c94b48a/rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:acc992ab27b15f852c76755eb2ab7dce86585ddadba6fa5946e58556088845b4", size = 373783, upload-time = "2026-06-30T07:14:57.736Z" }, + { url = "https://files.pythonhosted.org/packages/87/fe/e80107ee3639585c9941c17d6a42cd65325022f656c023191fce78c324c8/rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7f88d653e7b3b779d71ae7454e20dcc9b6bae903f33c269db9f2be41bda3f261", size = 378316, upload-time = "2026-06-30T07:14:59.077Z" }, + { url = "https://files.pythonhosted.org/packages/22/6f/81e3adf81acfb6fa694de2a6e4e7d8863121e3e0799e0a7725e6cf5679c4/rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e52655eaf81e32593abedaa4bfe33170c8cfedf3365ed9be6e11e07f148f0278", size = 499423, upload-time = "2026-06-30T07:15:00.488Z" }, + { url = "https://files.pythonhosted.org/packages/2d/9a/41263969df0ce3d9af2a96d5005a288200af1989aed3354bfceb5fc0b21f/rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dfcc8b909769d19db55c7cc9541eb64b9b774b1057ffffb4f1048070475bb9f9", size = 386077, upload-time = "2026-06-30T07:15:01.911Z" }, + { url = "https://files.pythonhosted.org/packages/5e/19/7e98f468bd50346faff5b10e5297374b443bfdddacc8e9fbc65984539597/rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c1255b302953c86a486b81d330d5ee1d5bd937691ce271b6be0ef0e299eaab7", size = 371315, upload-time = "2026-06-30T07:15:03.317Z" }, + { url = "https://files.pythonhosted.org/packages/99/3c/2b973b4d371906a134b03decfea7f5d9835a2c6d263454392e15b64b5b18/rpds_py-2026.6.3-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:8d2294a31386bfa251d8c8a39472beee17db67d4f1a6eabea665d35c9a4461c3", size = 383502, upload-time = "2026-06-30T07:15:04.627Z" }, + { url = "https://files.pythonhosted.org/packages/98/2a/12e2799500af0a307bca76b63361c51f9fe479223561489c29eea1f2ee41/rpds_py-2026.6.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f8f23ead891a3b762f35ab3b04623da7056545b48aa60d59957e6789914545da", size = 402673, upload-time = "2026-06-30T07:15:05.856Z" }, + { url = "https://files.pythonhosted.org/packages/2d/e3/21e5872d165fe08be4f229e3d5ee9d90019c0bf0e5538de60dbd54009450/rpds_py-2026.6.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:421aba32367055614287a4292b6a17f1939c9452299f7a0209c117e990b646d4", size = 549964, upload-time = "2026-06-30T07:15:07.159Z" }, + { url = "https://files.pythonhosted.org/packages/1a/d0/5ee0fe36844297de8123bee27bc12078c1a7416ad9f1b8a8ca18d6b0c0ac/rpds_py-2026.6.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1e5822dfc2f0d4ab7e745eaa6d85945069329beeccef965af3f3bb26058fcab6", size = 615446, upload-time = "2026-06-30T07:15:08.531Z" }, + { url = "https://files.pythonhosted.org/packages/b1/80/1ea5873cb683f2fbe5f21b23ea1f6d179ead19f3c5b249b7eb5dca568ef2/rpds_py-2026.6.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:83e35b57523816c8613fd0776b40cd8bb9f596b37ddd2692eb4a6bb5ab2f8c93", size = 576975, upload-time = "2026-06-30T07:15:09.97Z" }, + { url = "https://files.pythonhosted.org/packages/c9/e1/90ef639217a5ddb15b7f4f61b1c33911fd044ad03c311bafdd2bcab85582/rpds_py-2026.6.3-cp311-cp311-win32.whl", hash = "sha256:de3eceba0b683bcbb1ab93da016d0270df1f9ae7be716b40214c5dafac6ea45a", size = 204453, upload-time = "2026-06-30T07:15:11.324Z" }, + { url = "https://files.pythonhosted.org/packages/f2/b7/b7a1695d7af36f521fb11e80d6d3adbd744f73b921859bd3c2a2c0dc706f/rpds_py-2026.6.3-cp311-cp311-win_amd64.whl", hash = "sha256:2c54a076ca4d370980ab57bc0e31df57bbe8d41340436a90ef8b1219a3cbb127", size = 223219, upload-time = "2026-06-30T07:15:12.476Z" }, + { url = "https://files.pythonhosted.org/packages/d7/a2/145afacf796e4506062825941176ad9445c2dcf2b3b6a1f13d3030a15e19/rpds_py-2026.6.3-cp311-cp311-win_arm64.whl", hash = "sha256:168c733a7112e071bb7a66460e667edfcff06c017a3c523f7a8a8e08d0140804", size = 219137, upload-time = "2026-06-30T07:15:13.631Z" }, + { url = "https://files.pythonhosted.org/packages/5c/be/2e8974163072e7bab7df1a5acd54c4498e75e35d6d18b864d3a9d5dadc92/rpds_py-2026.6.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a0811d33247c3d6128a3001d763f2aa056bb3425204335400ac54f89eec3a0d0", size = 343691, upload-time = "2026-06-30T07:15:14.96Z" }, + { url = "https://files.pythonhosted.org/packages/a4/73/319dfa745dd668efe89309141ded489126461fcecd2b8f3a3cda185129b6/rpds_py-2026.6.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:538949e262e46caa31ac01bdb3c1e8f642622922cacbabbae6a8445d9dc33eaf", size = 338542, upload-time = "2026-06-30T07:15:16.267Z" }, + { url = "https://files.pythonhosted.org/packages/21/63/4239893be1c4d09b709b1a8f6be4188f0870084ff547f46606b8a75f1b03/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55927d532399c2c646100ff7feb48eaa940ad70f42cd68e1328f3ded9f81ca24", size = 368180, upload-time = "2026-06-30T07:15:17.62Z" }, + { url = "https://files.pythonhosted.org/packages/1c/ca/9c5de382225234ceb37b1844ebdb140db12b2a278bb9efe2fcd19f6c82ce/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f56f1695bc5c0871cbc33dc0130fcf503aab0c57dcc5a6700a4f49eba4f2652e", size = 375067, upload-time = "2026-06-30T07:15:18.952Z" }, + { url = "https://files.pythonhosted.org/packages/87/dc/863f69d1bf04ade34b7fe0d59b9fdf6f0135fe2d7cbca74f1d665589559d/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:270b293dae9058fc9fcedab50f13cebf46fb8ed1d1d54e0521a9da5d6b211975", size = 490509, upload-time = "2026-06-30T07:15:20.434Z" }, + { url = "https://files.pythonhosted.org/packages/ce/ef/eac16a12048b45ec7c7fa94f2be3438a5f26bf9cc8580b18a1cfd609b7f6/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:127565fead0a10943b282957bd5447804ff3160ad79f2ad2635e6d249e380680", size = 382754, upload-time = "2026-06-30T07:15:21.831Z" }, + { url = "https://files.pythonhosted.org/packages/04/8f/d2f3f532616be4d06c316ef119683e832bd3d41e112bf3a88f4151c95b17/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ecabd69db66de867690f9797f2f8fa27ba501bbc24540cbdbdc649cd15888ba6", size = 366189, upload-time = "2026-06-30T07:15:23.371Z" }, + { url = "https://files.pythonhosted.org/packages/e3/29/41a7b0e98a4b44cd676ab7598419623373eb43b20be68c084935c1a8cf88/rpds_py-2026.6.3-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:58eadac9cd119677b60e1cf8ac4052f35949d71b8a9e5556efccbe82533cf22a", size = 377750, upload-time = "2026-06-30T07:15:24.659Z" }, + { url = "https://files.pythonhosted.org/packages/2e/05/ecda0bec46f9a1565090bcdc941d023f6a25aff85fda28f89f8d19878152/rpds_py-2026.6.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7491ee23305ac3eb59e492b6945881f5cd77a6f731061a3f25b77fd40f9e99a4", size = 395576, upload-time = "2026-06-30T07:15:25.987Z" }, + { url = "https://files.pythonhosted.org/packages/68/a8/6ed52f03ee6cb854ce78785cc9a9a672eb880e83fd7224d471f667d151f1/rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2c99f7e8ccb3dd6e3e4bfeac657a7b208c9bac8075f4b078c02d7404c34107fa", size = 543807, upload-time = "2026-06-30T07:15:27.356Z" }, + { url = "https://files.pythonhosted.org/packages/8f/d6/156c0d3eea27ba09b92562ba2364ba124c0a061b199e17eac637cd25a5e2/rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:62698275682bf121181861295c9181e789030a2d516071f5b8f3c23c170cd0fc", size = 611187, upload-time = "2026-06-30T07:15:28.931Z" }, + { url = "https://files.pythonhosted.org/packages/f1/31/774212ed989c62f7f310220089f9b0a3fb8f40f5443d1727abd5d9f52bc9/rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a214c993455f99a89aaeadc9b21241900037adc9d97203e374d75513c5911822", size = 573030, upload-time = "2026-06-30T07:15:30.553Z" }, + { url = "https://files.pythonhosted.org/packages/c9/50/22f73127a41f1ce4f87fe39aadfb9a126345801c274aa93ae88456249327/rpds_py-2026.6.3-cp312-cp312-win32.whl", hash = "sha256:501f9f04a588d6a09179368c57071301445191767c64e4b52a6aa9871f1ef5ed", size = 202185, upload-time = "2026-06-30T07:15:32.027Z" }, + { url = "https://files.pythonhosted.org/packages/04/3a/f0ee4d4dde9d3b69dedf1b5f74e7a40017046d55052d173e418c6a94f960/rpds_py-2026.6.3-cp312-cp312-win_amd64.whl", hash = "sha256:2c958bf94822e9290a40aaf2a822d4bc5c88099093e3948ad6c571eca9272e5f", size = 220394, upload-time = "2026-06-30T07:15:33.359Z" }, + { url = "https://files.pythonhosted.org/packages/f3/83/3382fe37f809b59f02aac04dbc4e765b480b46ee0227ed516e3bdc4d3dfc/rpds_py-2026.6.3-cp312-cp312-win_arm64.whl", hash = "sha256:22bffe6042b9bcb0822bcd1955ec00e245daf17b4344e4ed8e9551b976b63e96", size = 215753, upload-time = "2026-06-30T07:15:34.778Z" }, + { url = "https://files.pythonhosted.org/packages/a4/9e/b818ee580026ec578138e961027a68820c40afeb1ec8f6819b54fb99e196/rpds_py-2026.6.3-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:3cfe765c1da0072636ca06628261e0ea05688e160d5c8a03e0217c3854037223", size = 343012, upload-time = "2026-06-30T07:15:36.005Z" }, + { url = "https://files.pythonhosted.org/packages/f3/6b/686d9dc4359a8f163cfbbf89ee0b4e586431de22fe8248edb63a8cf50d49/rpds_py-2026.6.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f4d78253f6996be4901669ad25319f842f740eccf4d58e3c7f3dd39e6dde1d8f", size = 338203, upload-time = "2026-06-30T07:15:37.462Z" }, + { url = "https://files.pythonhosted.org/packages/9e/9b/069aa329940f8207615e091f5eedbbd40e1e15eac68a0790fd05ccdf796c/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54f45a148e28767bf343d33a684693c70e451c6f4c0e9904709a723fafbdfc1f", size = 367984, upload-time = "2026-06-30T07:15:39.008Z" }, + { url = "https://files.pythonhosted.org/packages/14/db/34c203e4becff3703e4d3bc121842c00b8689197f398161203a880052f4e/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:842e7b070435622248c7a2c44ae53fa1440e073cc3023bc919fed570884097a7", size = 374815, upload-time = "2026-06-30T07:15:40.253Z" }, + { url = "https://files.pythonhosted.org/packages/ee/7d/8071067d2cc453d916ad836e828c943f575e8a44612537759002a1e07381/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8020133a74bd81b4572dd8e4be028a6b1ebcd70e6726edc3918008c08bee6ee6", size = 490545, upload-time = "2026-06-30T07:15:41.729Z" }, + { url = "https://files.pythonhosted.org/packages/a3/42/da06c5aa8f0484ff07f270787434204d9f4535e2f8c3b51ed402267e63c3/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cdc7e35386f3847df728fbcb5e887e2d79c19e2fa1eba9e51b6621d23e3243af", size = 382828, upload-time = "2026-06-30T07:15:43.327Z" }, + { url = "https://files.pythonhosted.org/packages/57/d7/fe978efc2ae50abe48eb7464668ea99f53c010c60aeebb7b35ad27f23661/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acac386b453c2516111b50985d60ce46e7fadb5ea71ae7b25f4c946935bf27cf", size = 365678, upload-time = "2026-06-30T07:15:44.992Z" }, + { url = "https://files.pythonhosted.org/packages/69/9d/1d8922e1990b2a6eb532b6ff53d3e73d2b3bbffc84116c75826bee73dfc6/rpds_py-2026.6.3-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:425560c6fa0415f27261727bb20bd097568485e5eb0c121f1949417d1c516885", size = 377811, upload-time = "2026-06-30T07:15:46.523Z" }, + { url = "https://files.pythonhosted.org/packages/b1/3d/198dceafb4fb034a6a47347e1b0735d34e0bd4a50be4e898d408ee66cb14/rpds_py-2026.6.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a550fb4950a06dde3beb4721f5ad4b25bf4513784665b0a8522c792e2bd822a4", size = 395382, upload-time = "2026-06-30T07:15:47.955Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f1/13968e49655d40b6b19d8b9140296bbc6f1d86b3f0f6c346cf9f1adddf4b/rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4f4bca01b63096f606e095734dd56e74e175f94cfbf24ff3d63281cec61f7bb7", size = 543832, upload-time = "2026-06-30T07:15:49.33Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ab/289bcb1b90bd3e40a2900c561fa0e2087345ecbb094f0b870f2345142b7c/rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ccffae9a092a00deb7efd545fe5e2c33c33b88e7c054337e9a74c179347d0b7d", size = 611011, upload-time = "2026-06-30T07:15:50.847Z" }, + { url = "https://files.pythonhosted.org/packages/1e/16/5043105e679436ccfbc8e5e0dd2d663ed18a8b8113515fd06a5e5d77c83e/rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1cf01971c4f2c5553b772a542e4aaf191789cd331bc2cd4ff0e6e65ba49e1e97", size = 572431, upload-time = "2026-06-30T07:15:52.394Z" }, + { url = "https://files.pythonhosted.org/packages/85/ed/adab103321c0a6565d5ae1c2998349bc3ee175b82ccc5ae8fc04cc413075/rpds_py-2026.6.3-cp313-cp313-win32.whl", hash = "sha256:8c3d1e9c15b9d51ca0391e13da1a25a0a4df3c58a37c9dc368e0736cf7f69df0", size = 201710, upload-time = "2026-06-30T07:15:53.894Z" }, + { url = "https://files.pythonhosted.org/packages/7b/ed/a03b09668e74e5dabbf2e211f6468e1820c0552f7b0500082da31841bf7b/rpds_py-2026.6.3-cp313-cp313-win_amd64.whl", hash = "sha256:9250a9a0a6fd4648b3f868da8d91a4c52b5811a62df58e753d50ae4454a36f80", size = 219454, upload-time = "2026-06-30T07:15:55.25Z" }, + { url = "https://files.pythonhosted.org/packages/27/17/b8642c12930b71bc2b25831f6708ccf0f75abcd11883932ec9ce54ba3a78/rpds_py-2026.6.3-cp313-cp313-win_arm64.whl", hash = "sha256:900a67df3fd1660b035a4761c4ce73c382ea6b35f90f9863c36c6fd8bf8b09bb", size = 215063, upload-time = "2026-06-30T07:15:56.573Z" }, + { url = "https://files.pythonhosted.org/packages/b6/36/7fbe9dcdaf857fb3f63c2a2284b62492d95f5e8334e947e5fb6e7f68c9be/rpds_py-2026.6.3-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:931908d9fc855d8f74783377822be318edb6dcb19e47169dc038f9a1bf60b06e", size = 344510, upload-time = "2026-06-30T07:15:57.921Z" }, + { url = "https://files.pythonhosted.org/packages/ba/54/f785cc3d3f60839ca57a5af4927a9f347b07b2799c373fc20f7949f87c7e/rpds_py-2026.6.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d7469697dce35be237db177d42e2a2ee26e6dcc5fc052078a6fefabd288c6edd", size = 339495, upload-time = "2026-06-30T07:15:59.238Z" }, + { url = "https://files.pythonhosted.org/packages/63/ef/d4cdaf309e6b095b43597103cf8c0b951d6cca2acce68c474f75ec12e0c7/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bcfbcf66006befb9fd2aeaa9e01feaf881b4dc330a02ba07d2322b1c11be7b5d", size = 369454, upload-time = "2026-06-30T07:16:01.021Z" }, + { url = "https://files.pythonhosted.org/packages/96/4a/9559a68b7ee15db09d7981212e8c2e219d2a1d6d4faa0391d813c3496a36/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:847927daf4cffbd4e90e42bc890069897101edd015f956cb8721b3473372edda", size = 374583, upload-time = "2026-06-30T07:16:02.287Z" }, + { url = "https://files.pythonhosted.org/packages/ef/75/8964aa7d2c6e8ac43eba8eb6e6b0fdda1f46d39f2fc3e6aa9f2cb17f485d/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aca6c1ef08a82bfe327cc156da694660f599923e2e6665b6d81c9c2d0ac9ffc8", size = 492919, upload-time = "2026-06-30T07:16:03.723Z" }, + { url = "https://files.pythonhosted.org/packages/8f/97/6908094ac804115e65aedfd90f1b5fee4eebebd3f6c4cfc5419939267565/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ae50181a047c871561212bb97f7932a2d45fb53e947bd9b57ebad85b529cbc53", size = 383725, upload-time = "2026-06-30T07:16:05.305Z" }, + { url = "https://files.pythonhosted.org/packages/d1/9c/0d1fdc2e7aba23e290d603bc494e97bd205bae262ce33c6b32a69768ed5e/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc319e5a1de4b6913aac94bf6a2f9e847371e0a140a43dd4991db1a09bc2d504", size = 367255, upload-time = "2026-06-30T07:16:07.086Z" }, + { url = "https://files.pythonhosted.org/packages/c4/fe/f0209ca4a9ed074bc8acb44dfd0e81c3122e94c9689f5645b7973a866719/rpds_py-2026.6.3-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:e4316bf32babbed84e691e352faf967ce2f0f024174a8643c37c94a1080374fc", size = 379060, upload-time = "2026-06-30T07:16:08.525Z" }, + { url = "https://files.pythonhosted.org/packages/c6/8d/f1cc54c616b9d8897de8738aac148d20afca93f68187475fe194d09a71b9/rpds_py-2026.6.3-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8c6e5a2f750cc71c3e3b11d71661f21d6f9bc6cebc6564b1466417a1ec03ec77", size = 395960, upload-time = "2026-06-30T07:16:09.989Z" }, + { url = "https://files.pythonhosted.org/packages/fb/04/aafff00f73aeca2945f734f1d483c64ab8f472d0864ab02377fd8e89c3b2/rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4470ce197d4090875cf6affbf1f853338387428df97c4fb7b7106317b8214698", size = 545356, upload-time = "2026-06-30T07:16:11.816Z" }, + { url = "https://files.pythonhosted.org/packages/fd/cc/e229663b9e4ddac5a4acbe9085dd80a71af2a5d356b8b39d6bff233f24b0/rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ea964164cc9afa72d4d9b23cc28dafae93693c0a53e0b42acbff15b22c3f9ddd", size = 612319, upload-time = "2026-06-30T07:16:13.586Z" }, + { url = "https://files.pythonhosted.org/packages/e3/7a/8a0e6d3e6cd066af108b71b43122c3fe158dd9eb86acac626593a2582eb1/rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:639c8929aa0afe81be836b04de888460d6bed38b9c54cfc18da8f6bfabf5af5d", size = 573508, upload-time = "2026-06-30T07:16:15.23Z" }, + { url = "https://files.pythonhosted.org/packages/87/03/2a69ab618a789cf6cf85c86bb844c62d090e700ab1a2aa676b3741b6c516/rpds_py-2026.6.3-cp314-cp314-win32.whl", hash = "sha256:882076c00c0a608b131187055ddc5ae29f2e7eaf870d6168980420d58528a5c8", size = 202504, upload-time = "2026-06-30T07:16:16.893Z" }, + { url = "https://files.pythonhosted.org/packages/85/62/a3892ba945f4e24c78f352e5de3c7620d8479f73f211406a97263d13c7d2/rpds_py-2026.6.3-cp314-cp314-win_amd64.whl", hash = "sha256:0be972be84cfcaf46c8c6edf690ca0f154ac17babf1f6a955a51579b34ad2dc5", size = 220380, upload-time = "2026-06-30T07:16:18.108Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e7/c2bd44dc831931815ad11ebb5f430b5a0a4d3caa9de837107876c30c3432/rpds_py-2026.6.3-cp314-cp314-win_arm64.whl", hash = "sha256:2a9c6f195058cb45335e8cc3802745c603d716eb96bc9625950c1aac71c0c703", size = 215976, upload-time = "2026-06-30T07:16:19.654Z" }, + { url = "https://files.pythonhosted.org/packages/79/9c/fff7b74bce9a091ec9a012a03f9ff5f69364eaf9451060dfc4486da2ffdd/rpds_py-2026.6.3-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:f90938e92afda60266da758ee7d363447f7f0138c9559f9e1811629580582d90", size = 346840, upload-time = "2026-06-30T07:16:21.268Z" }, + { url = "https://files.pythonhosted.org/packages/e9/44/77bcb1168b33704908295533d27f10eb811e9e3e193e8993dc99572211d3/rpds_py-2026.6.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ec829541c45bca16e61c7ae50c20501f213605beb75d1aba91a6ee37fbbb56a4", size = 340282, upload-time = "2026-06-30T07:16:22.875Z" }, + { url = "https://files.pythonhosted.org/packages/87/3c/7a9081c7c9e645b39efe19e4ffbeccd80add246327cd9b888aecffd72317/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afd70d95892096cdb26f15a00c45907b17817577aa8d1c76b2dcc2788391f9e9", size = 370403, upload-time = "2026-06-30T07:16:24.415Z" }, + { url = "https://files.pythonhosted.org/packages/f7/69/af47021eb7dad6ff3396cb001c08f0f3c4d06c20253f75be6421a59fe6b7/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:29dfa0533a5d4c94d4dfa1b694fcb56c9c63aad8330ffdd816fd225d0a7a162f", size = 376055, upload-time = "2026-06-30T07:16:26.111Z" }, + { url = "https://files.pythonhosted.org/packages/81/fc/a3bcf517084396a6dd258c592567a3c011ba4557f2fde23dceaf26e74f2e/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:af05d726809bff6b141be124d4c7ce998f9c9c7f30edb1f46c07aa103d540b41", size = 494419, upload-time = "2026-06-30T07:16:27.596Z" }, + { url = "https://files.pythonhosted.org/packages/c9/eb/13d529d1788135425c7bf207f8463458ca5d92e43f3f701365b83e9dffc1/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9826217f048f620d9a712672818bf231442c1b35d96b227a07eabd11b4bb6945", size = 384848, upload-time = "2026-06-30T07:16:29.183Z" }, + { url = "https://files.pythonhosted.org/packages/8e/f4/b7ac49f30013aba8f7b9566b1dd07e81de95e708c1374b7bacc5b9bc5c9c/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:536bceea4fa4acf7e1c61da2b5786304367c816c8895be71b8f537c480b0ea1f", size = 371369, upload-time = "2026-06-30T07:16:30.912Z" }, + { url = "https://files.pythonhosted.org/packages/31/86/6260bafa622f788b07ddec0e52d810305c8b9b0b8c27f58a2ab04bf62b4f/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:bc0011654b91cc4fb2ae701bec0a0ba1e552c0714247fa7af6c59e0ccfa3a4e1", size = 379673, upload-time = "2026-06-30T07:16:32.486Z" }, + { url = "https://files.pythonhosted.org/packages/19/c3/03f1ee79a047b48daeca157c89a18509cde22b6b951d642b9b0af1be660a/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:539d75de9e0d536c84ff18dfeb805398e58227001ce09231a26a08b9aed1ee0e", size = 397500, upload-time = "2026-06-30T07:16:34.471Z" }, + { url = "https://files.pythonhosted.org/packages/f0/95/8ed0cd8c377dca12aea498f119fe639fc474d1461545c39d2b5872eb1c0f/rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:166cf54d9f44fc6ceb53c7860258dde44a81406646de79f8ed3234fca3b6e538", size = 545978, upload-time = "2026-06-30T07:16:36.45Z" }, + { url = "https://files.pythonhosted.org/packages/d3/f2/0eb57f0eaa83f8fc152a7e03de968ab77e1f00732bebc892b190c6eebde7/rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:d34c20167764fbcf927194d532dd7e0c56772f0a5f943fa5ef9e9afbba8fb9db", size = 613350, upload-time = "2026-06-30T07:16:38.213Z" }, + { url = "https://files.pythonhosted.org/packages/5b/de/e0674bdbc3ef7634989b3f854c3f34bc1f587d36e5bfdc5c378d57034619/rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ea7bb13b7c9a29791f87a0387ba7d3ad3a6d783d827e4d3f27b40a0ff44495e2", size = 576486, upload-time = "2026-06-30T07:16:39.797Z" }, + { url = "https://files.pythonhosted.org/packages/f2/f6/21101359743cd136ada781e8210a85769578422ba460672eea0e29739200/rpds_py-2026.6.3-cp314-cp314t-win32.whl", hash = "sha256:6de4744d05bd1aa1be4ed7ea1189e3979196808008113bbbf899a460966b925e", size = 201068, upload-time = "2026-06-30T07:16:41.316Z" }, + { url = "https://files.pythonhosted.org/packages/a6/b2/9574d4d44f7760c2aa32d92a0a4f41698e33f5b204a0bf5c9758f52c79d5/rpds_py-2026.6.3-cp314-cp314t-win_amd64.whl", hash = "sha256:c7b9a2f8f4d8e90af72571d3d495deebdd7e3c75451f5b41719aee166e940fc2", size = 220600, upload-time = "2026-06-30T07:16:43.091Z" }, + { url = "https://files.pythonhosted.org/packages/08/ae/f23a2697e6ee6340a578b0f136be6483657bef0c6f9497b752bb5c0964bb/rpds_py-2026.6.3-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:e059c5dde6452b44424bd1834557556c226b57781dee1227af23518459722b13", size = 344726, upload-time = "2026-06-30T07:16:44.5Z" }, + { url = "https://files.pythonhosted.org/packages/c3/63/e7b3a1a5358dd32c930a1062d8e15b67fd6e8922e81df9e91706d66ee5c8/rpds_py-2026.6.3-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2f7c26fbc5acd2522b95d4177fe4710ffd8e9b20529e703ffbf8db4d93903f05", size = 339587, upload-time = "2026-06-30T07:16:46.255Z" }, + { url = "https://files.pythonhosted.org/packages/ec/64/10a85681916ca55fffb91b0a211f84e34297c109243484dd6394660a8a7c/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3086b538543802f84c843911242db20447de00d8752dd0efc936dbcf02218ba", size = 369585, upload-time = "2026-06-30T07:16:48.101Z" }, + { url = "https://files.pythonhosted.org/packages/76/c2/baf95c7c38823e12ba34407c5f5767a89e5cf2233895e56f608167ae9493/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8f2e5c5ee828d42cb11760761c0af6507927bec42d0ad5458f97c9203b054617", size = 375479, upload-time = "2026-06-30T07:16:49.93Z" }, + { url = "https://files.pythonhosted.org/packages/6a/94/0aad06c72d65101e11d33528d438cda99a39ce0da99466e156158f2541d3/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ed0c1e5d10cdc7135537988c74a0188da68e2f3c30813ba3744ab1e42e0480f9", size = 492418, upload-time = "2026-06-30T07:16:51.641Z" }, + { url = "https://files.pythonhosted.org/packages/b5/17/de3f5a479a1f056535d7489819639d8cd591ea6281d700390b43b1abd745/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c2642a7603ec0b16ed77da4555db3b4b472341904873788327c0b0d7b95f1bb", size = 384123, upload-time = "2026-06-30T07:16:53.622Z" }, + { url = "https://files.pythonhosted.org/packages/46/7d/bf09bd1b145bb2671c03e1e6d1ab8651858d90d8c7dfeadd85a37a934fd8/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e4320744c1ffdd95a603def63344bfab2d33edeab301c5007e7de9f9f5b3885", size = 367351, upload-time = "2026-06-30T07:16:55.241Z" }, + { url = "https://files.pythonhosted.org/packages/a3/ea/1bb734f314b8be319149ddee80b18bd41372bdcfbdf88d28131c0cd37719/rpds_py-2026.6.3-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:a9f4645593036b81bbdb36b9c8e0ea0d1c3fee968c4d59db0344c14087ef143a", size = 378827, upload-time = "2026-06-30T07:16:56.841Z" }, + { url = "https://files.pythonhosted.org/packages/4b/93/d9611e5b25e26df9a3649813ed66193ace9347a7c7fc4ab7cf70e94851c0/rpds_py-2026.6.3-cp315-cp315-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e55d236be29255554da47abe5c577637db7c24a02b8b46f0ca9524c855801868", size = 395966, upload-time = "2026-06-30T07:16:58.557Z" }, + { url = "https://files.pythonhosted.org/packages/c3/cb/99d77e16e5534ae1d90629bbe419ba6ee170833a6a85e3aa1cc41726fbbc/rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:24e9c5386e16669b674a69c156c8eeefcb578f3b3397b713b08e6d60f3c7b187", size = 545680, upload-time = "2026-06-30T07:17:00.164Z" }, + { url = "https://files.pythonhosted.org/packages/59/15/11a29755f790cef7a2f755e8e14f4f0c33f39489e1893a632a2eee59672b/rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:c60924535c75f1566b6eb75b5c31a48a43fef04fa2d0d201acbad8a9969c6107", size = 611853, upload-time = "2026-06-30T07:17:01.962Z" }, + { url = "https://files.pythonhosted.org/packages/68/86/0c27547e21644da938fb530f7e1a8148dd24d02db07e7a5f2567a17ce710/rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:38a2fea2787428f811719ceb9114cb78964a3138838320c29ac39526c79c16ba", size = 573715, upload-time = "2026-06-30T07:17:03.693Z" }, + { url = "https://files.pythonhosted.org/packages/29/71/4d8fcf700931815594bce892255bbd973b94efaf0fc1932b0590df18d886/rpds_py-2026.6.3-cp315-cp315-win32.whl", hash = "sha256:d483fe17f01ad64b7bf7cc38fcefff1ca9fb83f8c2b2542b68f97ffe0611b369", size = 202864, upload-time = "2026-06-30T07:17:05.746Z" }, + { url = "https://files.pythonhosted.org/packages/eb/62/b577562de0edbb55b2be85ce5fd09c33e386b9b13eee09833af4240fd5c4/rpds_py-2026.6.3-cp315-cp315-win_amd64.whl", hash = "sha256:67e3a721ffc5d8d2210d3671872298c4a84e4b8035cfe42ffd7cde35d772b146", size = 220430, upload-time = "2026-06-30T07:17:07.471Z" }, + { url = "https://files.pythonhosted.org/packages/c8/95/d6d0b2509825141eef60669a5739eec88dbc6a48053d6c92993a5704defe/rpds_py-2026.6.3-cp315-cp315-win_arm64.whl", hash = "sha256:6e84adbcf4bf841aed8116a8264b9f50b4cb3e7bd89b516122e616ac56ca269e", size = 215877, upload-time = "2026-06-30T07:17:09.008Z" }, + { url = "https://files.pythonhosted.org/packages/b7/bf/f3ea278f0afd615c1d0f19cb69043a41526e2bb600c2b536eb192218eb27/rpds_py-2026.6.3-cp315-cp315t-macosx_10_12_x86_64.whl", hash = "sha256:ae6dd8f10bd17aad820876d24caec9efdafd80a318d16c0a48edb5e136902c6b", size = 346933, upload-time = "2026-06-30T07:17:10.762Z" }, + { url = "https://files.pythonhosted.org/packages/9d/29/9907bdf1c5346763cf10b7f6852aad86652168c259def904cbe0082c5864/rpds_py-2026.6.3-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:bdbd97738551fca3917c1bd7188bec1920bb520104f28e7e1007f9ceb17b7690", size = 340274, upload-time = "2026-06-30T07:17:12.266Z" }, + { url = "https://files.pythonhosted.org/packages/6f/2c/8e03767b5778ef25cebf74a7a91a2c3806f8eced4c92cb7406bbe060756d/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b95977e7211527ab0ba576e286d023389fbeeb32a6b7b771665d333c60e5342", size = 370763, upload-time = "2026-06-30T07:17:14.107Z" }, + { url = "https://files.pythonhosted.org/packages/2e/e1/df2a7e1ba2efd796af26194250b8d42c821b46592311595162af9ef0528d/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d15fde0e6fb0d88a60d221204873743e5d9f0b7d29165e62cd86d0413ad74ba6", size = 376467, upload-time = "2026-06-30T07:17:15.76Z" }, + { url = "https://files.pythonhosted.org/packages/6b/de/8a0814d1946af29cb068fb259aa8622f856df1d0bab58429448726b537f5/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a136d453475ac0fcbda502ef1e6504bd28d6d904700915d278deeab0d00fe140", size = 496689, upload-time = "2026-06-30T07:17:17.308Z" }, + { url = "https://files.pythonhosted.org/packages/df/f3/f19e0c852ba13694f5a79f3b719331051573cb5693feacf8a88ffffc3a71/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f826877d462181e5eb1c26a0026b8d0cab05d99844ecb6d8bf3627a2ca0c0442", size = 385340, upload-time = "2026-06-30T07:17:18.928Z" }, + { url = "https://files.pythonhosted.org/packages/e2/ae/7ec3a9d2d4351f99e37bcb06b6b6f954512646bfdbf9742e1de727865daf/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:79486287de1730dbaff3dbd124d0ca4d2ef7f9d29bf2544f1f93c09b5bcbbd12", size = 372179, upload-time = "2026-06-30T07:17:20.539Z" }, + { url = "https://files.pythonhosted.org/packages/d3/ac/9cee911dff2aaa9a5a8354f6610bf2e6a616de9197c5fff4f54f82585f1e/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_31_riscv64.whl", hash = "sha256:808345f53cb952433ca2816f1604ff3515608a81784954f38d4452acfe8e61d5", size = 379993, upload-time = "2026-06-30T07:17:22.212Z" }, + { url = "https://files.pythonhosted.org/packages/83/6b/7c2a07ba88d1e9a936612f7a5d067467ed03d971d5a06f7d309dff044a7e/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1967debc37f64f2c4dc90a7f563aec558b471966e12adcac4e1c4240496b6ebf", size = 398909, upload-time = "2026-06-30T07:17:23.66Z" }, + { url = "https://files.pythonhosted.org/packages/97/0b/776ffcb66783637b0031f6d58d6fb55913c8b5abf00aeecd46bf933fb477/rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:f0840b5b17057f7fd918b76183a4b5a0635f43e14eb2ce60dce1d4ee4707ea00", size = 546584, upload-time = "2026-06-30T07:17:25.264Z" }, + { url = "https://files.pythonhosted.org/packages/55/33/ba3bc04d7092bd553c9b2b195624992d2cc4f3de1f380b7b93cbee67bd79/rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:faa679d19a6696fd54259ad321251ad77a13e70e03dd834daa762a44fb6196ef", size = 614357, upload-time = "2026-06-30T07:17:26.888Z" }, + { url = "https://files.pythonhosted.org/packages/8b/71/14edf065f04630b1a8472f7653cad03f6c478bcf95ea0e6aed55451e33ea/rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:23a439f31ccbeff1574e24889128821d1f7917470e830cf6544dced1c662262a", size = 576533, upload-time = "2026-06-30T07:17:28.546Z" }, + { url = "https://files.pythonhosted.org/packages/ba/76/65002b08596c389105720a8c0d22298b8dc25a4baf89b2ce431343c8b1de/rpds_py-2026.6.3-cp315-cp315t-win32.whl", hash = "sha256:913ca42ccad3f8cc6e292b587ae8ae49c8c823e5dce51a736252fc7c7cdfa577", size = 201204, upload-time = "2026-06-30T07:17:30.193Z" }, + { url = "https://files.pythonhosted.org/packages/8c/97/d855d6b3c322d1f27e26f5241c42016b56cf01377ea8ed348285f54652f0/rpds_py-2026.6.3-cp315-cp315t-win_amd64.whl", hash = "sha256:ae3d4fe8c0b9213624fdce7279d70e3b148b682ca20719ebd193a23ebfa47324", size = 220719, upload-time = "2026-06-30T07:17:31.788Z" }, + { url = "https://files.pythonhosted.org/packages/b4/9c/f0d19ac587fd0e4ab6b72cda355e9c5a6166b01ef7e064e437aef8eb9fef/rpds_py-2026.6.3-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:4cf2d36a2357e4d07bb5a4f98801265327b48256867816cfd2ceb001e9754a8f", size = 349791, upload-time = "2026-06-30T07:17:33.315Z" }, + { url = "https://files.pythonhosted.org/packages/38/c7/1d49d204c9fd2ee6c537601dc4c1ba921e03363ca576bfab94a00254ac9a/rpds_py-2026.6.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:30c6dc199b24a5e3e81d50da0f00858c5bbdb2617a750395687f4339c5818171", size = 352842, upload-time = "2026-06-30T07:17:34.897Z" }, + { url = "https://files.pythonhosted.org/packages/ac/e5/c0b5dc93cd0d4c06ce1f438907649514e2ea077bcd911e3154a51e96c38e/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9891e594296ab9dada6551c8e7b387b2721f27a67eecd528412e8906247a7b90", size = 382094, upload-time = "2026-06-30T07:17:36.514Z" }, + { url = "https://files.pythonhosted.org/packages/0d/54/ec0e907b4ca8d541112db352409bd15f871c9b243e0c92c9b5a46ae96f01/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b5c2dc92304aa48a4a60443b548bb12f12e119d4b72f314015e67b9e1be97fca", size = 388662, upload-time = "2026-06-30T07:17:38.235Z" }, + { url = "https://files.pythonhosted.org/packages/d3/f4/921c22a4fd0f1c1ac13a3996ffbf0aa67951e2c8ad0d1d9574938a2932e8/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:127e08c0642d880cf32ca47ec2a4a77b901f7e2dd1ad9762adb13955d72ffcc9", size = 504896, upload-time = "2026-06-30T07:17:39.689Z" }, + { url = "https://files.pythonhosted.org/packages/0b/1b/a114b972cefa1ab1cdb3c7bb177cd3844a12826c507c722d3a73516dbbaf/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8bb68f03f395eb793220b45c097bd4d8c32944393da0fad8b999efac0868fc8c", size = 391545, upload-time = "2026-06-30T07:17:41.336Z" }, + { url = "https://files.pythonhosted.org/packages/4e/98/af9b3db77d47fcbe6c8c1f36e2c2147ec70292819e99c325f871584a1c11/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a3450b693fde92133e9f51060568a4c31fcca76d5e53bbd611e689ca446517e9", size = 380059, upload-time = "2026-06-30T07:17:42.857Z" }, + { url = "https://files.pythonhosted.org/packages/c9/ba/0efd8668b97c1d26a61566386c636a7a7a09829e474fdf807caa15a2c844/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:5e8d07bddee435a2ff6f1920e18feff28d0bc4533e42f4bf6927fbd073312c41", size = 393235, upload-time = "2026-06-30T07:17:44.637Z" }, + { url = "https://files.pythonhosted.org/packages/62/90/8c139ee9690f73b0829f32647de6f40d826f8f443af6fa72644f96351aac/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3a83ae6c67b7676b9878378547ca8e93ed77a580037bcbcd1d32f739e1e6089c", size = 413008, upload-time = "2026-06-30T07:17:46.225Z" }, + { url = "https://files.pythonhosted.org/packages/9c/97/0043896fdd7828ce09a1d9a8b06433714d0960fc4ff3fc4aa72b666b764e/rpds_py-2026.6.3-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:2bfd04c19ddbd6640de0b51894d764bd2758854d5b75bd102d2ef10cb9c293a9", size = 558118, upload-time = "2026-06-30T07:17:47.759Z" }, + { url = "https://files.pythonhosted.org/packages/f6/40/02355f0e134f783a8f9814c4680a1bd311d37671577a5964ea838573ff37/rpds_py-2026.6.3-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:ca6546b66be9dc4738b1b043d5ebd5488c66c578c5ff0fd0e8065313fe3afb76", size = 623138, upload-time = "2026-06-30T07:17:49.355Z" }, + { url = "https://files.pythonhosted.org/packages/10/85/48f0abdcef5cce4e034c7a5b0ceeceba0b01bf0d942824f4bb720afe2dec/rpds_py-2026.6.3-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:8e65860d238379ed982fd9ba690579b5e95af2f4840f99c772816dbe573cb826", size = 586486, upload-time = "2026-06-30T07:17:51.141Z" }, +] + +[[package]] +name = "ruff" +version = "0.15.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3a/06/ae069393fc66e8ff33036d4b368003833bf6e88ccf182e17e7a2f1c754fd/ruff-0.15.22.tar.gz", hash = "sha256:3f15175b1fb580126f58285a5dae6b2ea89000136d980c64499211f116b54809", size = 4785063, upload-time = "2026-07-16T15:14:13.244Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/23/18/ee54b7ae1e121be7a28ea6da4b67564ebb0530e183a54415ab7e3bcd2c4e/ruff-0.15.22-py3-none-linux_armv6l.whl", hash = "sha256:44423e73493737f5e7c5b41d475483898ff37afcdae38bc3da5085e29af1c2d8", size = 10781258, upload-time = "2026-07-16T15:13:19.452Z" }, + { url = "https://files.pythonhosted.org/packages/2f/d2/2520cb14761ddbeaf57642a76942fc36adcbdbe53b4532241995f6fc485c/ruff-0.15.22-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b82c6482946e9eda7ff2e091d25b8bad3f718684e1916d41bd56873cee05b697", size = 10999477, upload-time = "2026-07-16T15:13:23.318Z" }, + { url = "https://files.pythonhosted.org/packages/c9/10/74e53572aa758dfaa678c2a2646b5c5515d884b7ca56be4d2ce03ca4b560/ruff-0.15.22-py3-none-macosx_11_0_arm64.whl", hash = "sha256:11c1c715af53a09f714e011106bffc419751ec8232fcb5da42173284ea3fec6f", size = 10466716, upload-time = "2026-07-16T15:13:26.162Z" }, + { url = "https://files.pythonhosted.org/packages/1e/cc/44eaaf0844e028182f2d0a8f2190d0f359159aed0a9e5ab861d892f1ae2a/ruff-0.15.22-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:742a29cf29bddb7c8327895d6a10e0e6c5b38a96dd407af9b5d0857f809c0576", size = 10892644, upload-time = "2026-07-16T15:13:29.229Z" }, + { url = "https://files.pythonhosted.org/packages/9f/21/8edf559014d2b0f82beea19cfb713993ad802ccda16868769979c6090a84/ruff-0.15.22-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72af58b951b0ae395935ae79763dc349bc0eb706319d28f7a33ad2cfb3cfc178", size = 10576719, upload-time = "2026-07-16T15:13:32.35Z" }, + { url = "https://files.pythonhosted.org/packages/bf/1e/3a13abd392a3b50b62e5938a831f9ab6e588358cacad5c18545b716d2182/ruff-0.15.22-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62d425005c1835eb24e2ee4161cb90e8db263415f4a71c8c72c33abaa6c0c224", size = 11376494, upload-time = "2026-07-16T15:13:35.958Z" }, + { url = "https://files.pythonhosted.org/packages/bf/3e/422d3d95bcf04dd78e1aeac22184d4f9a8fb2c01865d39d44618484a0317/ruff-0.15.22-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e8b9b3f8779a4f08c969defc3c8c35abffaa757e601ed5ae66d6d1db6519969a", size = 12208370, upload-time = "2026-07-16T15:13:39.185Z" }, + { url = "https://files.pythonhosted.org/packages/1e/91/5d065a0e0a02bf4813f5119ad278462eed081d2b832eb7c021ade0ec9e65/ruff-0.15.22-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1e0dd1b2e4d3d585f897a0d137cbf4eaf6223bef4e8ce34d6bb12556c5f9249e", size = 11581098, upload-time = "2026-07-16T15:13:42.132Z" }, + { url = "https://files.pythonhosted.org/packages/f6/f9/a0d4871d12fae702eb1f41b686caf05f1f8b124dc6db6f784f53d74918fa/ruff-0.15.22-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:365523eb91d9224e1bcb03b022fbf0facb8f9e23792a2c53d9d4b3924bdbdebb", size = 11399422, upload-time = "2026-07-16T15:13:45.2Z" }, + { url = "https://files.pythonhosted.org/packages/18/80/c843a5176cddbceb0b7e8dd41cf9993490796c1c469348d384f5a5c13c56/ruff-0.15.22-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:fabfd168afdf29fee5be98b831efa9683c94d7c5a3b58b9ce5a2e38444589a74", size = 11381683, upload-time = "2026-07-16T15:13:48.46Z" }, + { url = "https://files.pythonhosted.org/packages/d4/00/8485de0ae92239438a36cfc51350db9b9e85c9ebdfaea91b18e422706662/ruff-0.15.22-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:225dbf095a87f1d9f90f5fd7924d2613ee452a75a4308c63a8f50f761787aa7c", size = 10850295, upload-time = "2026-07-16T15:13:51.655Z" }, + { url = "https://files.pythonhosted.org/packages/fa/91/24977ec2ec72eaf15e4394ace2959fdff2dd1e14f03e005e838023407169/ruff-0.15.22-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:1877d63b9d24ed278744f1523fd11b85540566d54641f97c566d7d9dc5ca5296", size = 10579640, upload-time = "2026-07-16T15:13:54.79Z" }, + { url = "https://files.pythonhosted.org/packages/9c/47/9b51216951974df1f263ac19da550d34252e0ed7218c25f10c5ef9ed7517/ruff-0.15.22-py3-none-musllinux_1_2_i686.whl", hash = "sha256:a1606c510bd7215680d32efab38965f7cdec3ef69f5170a3f4791404ffdd5262", size = 11105077, upload-time = "2026-07-16T15:13:57.915Z" }, + { url = "https://files.pythonhosted.org/packages/c2/47/20e9d4a3b8016778acea5fc32bb50d35d207500a17ddb529ffa6996feef8/ruff-0.15.22-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:630479b18625f5ffc373f77603a22a9f8ac0acd7ff0501178b5db28ec71e9c64", size = 11490980, upload-time = "2026-07-16T15:14:01.032Z" }, + { url = "https://files.pythonhosted.org/packages/4d/76/3f72d8fc38c1cb77b38c56a70da9d0c17700cc1cc50f9649c9d3c8f5ba71/ruff-0.15.22-py3-none-win32.whl", hash = "sha256:e5ba0e4a13fd14abbed2a77b517a3911290c6c6c59ef67784328d1668fab76cf", size = 10789165, upload-time = "2026-07-16T15:14:04.16Z" }, + { url = "https://files.pythonhosted.org/packages/cb/46/4965251734c2b6fcdca1b1b187d20bcac3af0ee5b083b89c910bb961ce3a/ruff-0.15.22-py3-none-win_amd64.whl", hash = "sha256:9be63ba1eb936acd2d1342fb8337c356353706fce233b2a15a09a97037e6acde", size = 11938297, upload-time = "2026-07-16T15:14:07.316Z" }, + { url = "https://files.pythonhosted.org/packages/57/c9/e69b1ff4c8b69093ef08b8919ab767af0569666865b39c30a8795d88d3c6/ruff-0.15.22-py3-none-win_arm64.whl", hash = "sha256:e1168075b72158510839f250027659cdd78476f40507dd517892304c41318661", size = 11298172, upload-time = "2026-07-16T15:14:10.51Z" }, ] [[package]] @@ -3059,15 +3173,15 @@ wheels = [ [[package]] name = "sse-starlette" -version = "3.3.4" +version = "3.4.6" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, { name = "starlette" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/26/8c/f9290339ef6d79badbc010f067cd769d6601ec11a57d78569c683fb4dd87/sse_starlette-3.3.4.tar.gz", hash = "sha256:aaf92fc067af8a5427192895ac028e947b484ac01edbc3caf00e7e7137c7bef1", size = 32427, upload-time = "2026-03-29T09:00:23.307Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6c/10/a34c656829ffc1c4b22ef36d70d9ebb6b99c020e2aeb17cee5485099f028/sse_starlette-3.4.6.tar.gz", hash = "sha256:725f8a1bd6d26ae1b2c9610c0ef5065dfdd496f3988d28adcf8c4b49dc25c627", size = 32542, upload-time = "2026-07-20T14:16:32.201Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f8/7f/3de5402f39890ac5660b86bcf5c03f9d855dad5c4ed764866d7b592b46fd/sse_starlette-3.3.4-py3-none-any.whl", hash = "sha256:84bb06e58939a8b38d8341f1bc9792f06c2b53f48c608dd207582b664fc8f3c1", size = 14330, upload-time = "2026-03-29T09:00:21.846Z" }, + { url = "https://files.pythonhosted.org/packages/49/36/e10c1d1b7ca881d2625db2ec28508578499187bb1c389952c398474e1834/sse_starlette-3.4.6-py3-none-any.whl", hash = "sha256:56217ab4c9a9f9c5db7b21e08732d3e7c2b807f45231ad23de0551a24c4a41f6", size = 16516, upload-time = "2026-07-20T14:16:30.978Z" }, ] [[package]] @@ -3175,23 +3289,23 @@ wheels = [ [[package]] name = "tqdm" -version = "4.67.3" +version = "4.69.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/09/a9/6ba95a270c6f1fbcd8dac228323f2777d886cb206987444e4bce66338dd4/tqdm-4.67.3.tar.gz", hash = "sha256:7d825f03f89244ef73f1d4ce193cb1774a8179fd96f31d7e1dcde62092b960bb", size = 169598, upload-time = "2026-02-03T17:35:53.048Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/69/40407dfc835517f058b603dbf37a6df094d8582b015a51eddc988febbcb7/tqdm-4.69.0.tar.gz", hash = "sha256:700c5e85dcd5f009dd6222588a29180a193a748247a5d855b4d67db93d79a53b", size = 792569, upload-time = "2026-07-17T18:09:06.2Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl", hash = "sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf", size = 78374, upload-time = "2026-02-03T17:35:50.982Z" }, + { url = "https://files.pythonhosted.org/packages/fe/21/99a0cdaf54eb35e77623c41b5a2c9472ee4404bba687052791fe2aba6773/tqdm-4.69.0-py3-none-any.whl", hash = "sha256:9979978912be667a6ef21fd5d8abf54e324e63d82f7f43c360792ebc2bc4e622", size = 676680, upload-time = "2026-07-17T18:09:04.172Z" }, ] [[package]] name = "traitlets" -version = "5.14.3" +version = "5.15.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/eb/79/72064e6a701c2183016abbbfedaba506d81e30e232a68c9f0d6f6fcd1574/traitlets-5.14.3.tar.gz", hash = "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7", size = 161621, upload-time = "2024-04-19T11:11:49.746Z" } +sdist = { url = "https://files.pythonhosted.org/packages/57/a9/a2584b8313b89f94869ddb3c4074617a691de1812a614d2d50e32ca5a7a6/traitlets-5.15.1.tar.gz", hash = "sha256:7b1c07854fe25acb39e009bae49f11b79ff6cbb2f27999104e9110e7a6b53722", size = 163344, upload-time = "2026-06-03T12:26:06.181Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f", size = 85359, upload-time = "2024-04-19T11:11:46.763Z" }, + { url = "https://files.pythonhosted.org/packages/96/8d/1080ee4c231f361b6ce4470d556c8c435b67c7e0753aaa641497ee92f88b/traitlets-5.15.1-py3-none-any.whl", hash = "sha256:770a53705f84b81ac107e83a1b3328ff2dae16094d8fc3cfc004e4b22dfd8e92", size = 85858, upload-time = "2026-06-03T12:26:04.395Z" }, ] [[package]] @@ -3205,51 +3319,51 @@ wheels = [ [[package]] name = "ty" -version = "0.0.59" +version = "0.0.61" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/95/b0/84ae7b3bf6e3e9f57eb9635eeff5a80b36e57aa089f40be0fb5c384fa176/ty-0.0.59.tar.gz", hash = "sha256:53e53ffeed78ad59cd237fa8ea1316d2b94e13efdea9a945698acab549e005aa", size = 6145435, upload-time = "2026-07-12T20:22:02.781Z" } +sdist = { url = "https://files.pythonhosted.org/packages/92/63/6944925d0fe9a4bb9cc744e6c045a42bbd2ee4654c103190674577a36c3f/ty-0.0.61.tar.gz", hash = "sha256:acbf0d914cc7e2e57ccc440036af36114819e2a604a5ffb554e72e4ca7dd65a2", size = 6234957, upload-time = "2026-07-18T01:39:54.696Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/57/e8/650b42fbef4d48e6ca682b0b6e9b68fa8fcf55cbb0a6892ab89990018b6f/ty-0.0.59-py3-none-linux_armv6l.whl", hash = "sha256:f8fb08a767ef8f11ea3c537b9d77860726cc2bc39e6f77ad13c02d5b289f20a7", size = 11700328, upload-time = "2026-07-12T20:21:26.046Z" }, - { url = "https://files.pythonhosted.org/packages/22/ac/0ca3a89d5f59ae5f308e5e83428cac5f9143200767743e052fba90b4b81e/ty-0.0.59-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:c7f4d5630836c8a0ba13dd4ac7bdae080a7d6ebe965b817ff642dc961bcf2a53", size = 11494310, upload-time = "2026-07-12T20:21:28.491Z" }, - { url = "https://files.pythonhosted.org/packages/f3/f8/5076de6001cefbccd8e6dc8472262697e43308ff66b0e87c72abba136357/ty-0.0.59-py3-none-macosx_11_0_arm64.whl", hash = "sha256:872f6fb02c6db5553c4d5fb283b3d50f0985fb9a29a910e4fda4793a775c1926", size = 11026797, upload-time = "2026-07-12T20:21:30.879Z" }, - { url = "https://files.pythonhosted.org/packages/2e/0f/fca28481b6a138e2b798ad9fdc98a095475f9104948ba242fce4b477782b/ty-0.0.59-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2af8eefbfe806337770eec12c0c819c5f1b8f5b85f8369cb1cc9fa25234a2208", size = 11475304, upload-time = "2026-07-12T20:21:33.041Z" }, - { url = "https://files.pythonhosted.org/packages/08/4b/1fed8b81b389ef4bbc0400f19e05fc16496b162577779dc0e5fc65ac216c/ty-0.0.59-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0acf8b76a1c9a7ddef460b42475f6c76193164426ab080783af1c3175b4b999b", size = 11533131, upload-time = "2026-07-12T20:21:35.189Z" }, - { url = "https://files.pythonhosted.org/packages/5f/fc/04eec35e05a10e0fea1c6503a290ccc3935efda9c845aff64e83282c1af7/ty-0.0.59-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:043c2e00eb1d7475f928af7dedd71f69b64e69bfca55e36f4c968479e1373fc4", size = 12205932, upload-time = "2026-07-12T20:21:37.324Z" }, - { url = "https://files.pythonhosted.org/packages/a9/dd/a61de859659fa11b55917ad38340a8f2c61f5ae17d1874929f29084c6990/ty-0.0.59-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f0d688d857441df57f48fca66c029d85cf737c510e7be1d01144cdad1e58d968", size = 12758406, upload-time = "2026-07-12T20:21:39.525Z" }, - { url = "https://files.pythonhosted.org/packages/c6/e8/fa66f05997eab8ca75fc4f17320140e25467849e0cc75597f898cc22099c/ty-0.0.59-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a96c9f88394a3b42c737e2125b2330543f0d90a43b49761f377d96f8c3ee0d62", size = 12288176, upload-time = "2026-07-12T20:21:41.784Z" }, - { url = "https://files.pythonhosted.org/packages/15/68/0fca59963bd5123f42d5f7da50667e7a52e8e9615e3a16d8c2c0d3b2d143/ty-0.0.59-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f08dbcb268edcafcb152e59475b5b495ce28d0b340a395c09943557678f4d5a6", size = 12028471, upload-time = "2026-07-12T20:21:43.82Z" }, - { url = "https://files.pythonhosted.org/packages/e1/5b/cd7dabbbab392578f11179919da5c25d8c3322e5388a688f539ea0539603/ty-0.0.59-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:8812764b9a40fdc98df1272826e73a298ef56b06681135e643bcf90aad1896f7", size = 12297646, upload-time = "2026-07-12T20:21:45.76Z" }, - { url = "https://files.pythonhosted.org/packages/1d/37/2e9c94f0b383d8cbe1a35517ab470b7810bc9d7501603ab532bcd5be5e90/ty-0.0.59-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:fd53b8581641d8dad7bfac6d5ea589e91a883d6837e0b9a286fdae30722b7c69", size = 11432519, upload-time = "2026-07-12T20:21:47.694Z" }, - { url = "https://files.pythonhosted.org/packages/9d/0a/af93e9785200f11ac416cc20235fc2464c9bd978e791190684ea0e458795/ty-0.0.59-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:86da5872124a41877d95058bc17d33ddcff034b587eb5f1e2917ab88ba227dac", size = 11554993, upload-time = "2026-07-12T20:21:49.671Z" }, - { url = "https://files.pythonhosted.org/packages/4b/dd/651bf87e20d00376c81b19124756491cffaf20eb8bec05a8794e5a8cf641/ty-0.0.59-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6a233eef5f2fd4d894881e4a0aec83c9f172bfae1d787d6596ee1939fcc7723e", size = 11818230, upload-time = "2026-07-12T20:21:51.659Z" }, - { url = "https://files.pythonhosted.org/packages/16/50/c947c4155fea751d135b19affdf734bbce72a94e446b866cf0c62f8bed69/ty-0.0.59-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:7ff678c18b5f1e3128b75a35e50dee7908dea55155baa31cd790619d5014cbf5", size = 12135194, upload-time = "2026-07-12T20:21:53.796Z" }, - { url = "https://files.pythonhosted.org/packages/b1/13/e5feb138888de1e95037c843571bbbd4ac21bf0a190507468098599a321f/ty-0.0.59-py3-none-win32.whl", hash = "sha256:cf8abb4b8095c5fe39102b8127f5886db308c8d4600909ddbc905512ce9c8163", size = 11179249, upload-time = "2026-07-12T20:21:55.752Z" }, - { url = "https://files.pythonhosted.org/packages/76/dd/52914dcbeeba92c207de40ef7109a58dcb5527aeb21c8f8feb7402aa9e29/ty-0.0.59-py3-none-win_amd64.whl", hash = "sha256:1dde20a82243d24407869e5a608c2f15efddd5cefc662aef461a5af84bfb3f8b", size = 12251079, upload-time = "2026-07-12T20:21:58.1Z" }, - { url = "https://files.pythonhosted.org/packages/d4/8f/ac36fde77e223297454c1e0aeb8888c169eaacf3163bb609e3af942c88cb/ty-0.0.59-py3-none-win_arm64.whl", hash = "sha256:987043ee9e021f49493d9135891ac69c1affeee0d4ad4480c5fa4d9c975fc91b", size = 11650921, upload-time = "2026-07-12T20:22:00.348Z" }, + { url = "https://files.pythonhosted.org/packages/dc/cf/044f31523e2768e3e64b0ca2ec32f70b3a731d4a2caa6ea110baf26e251c/ty-0.0.61-py3-none-linux_armv6l.whl", hash = "sha256:148779b8675eac93f40ec58bd70037fe67537117f20a23272264f8f136d41336", size = 11891448, upload-time = "2026-07-18T01:39:18.449Z" }, + { url = "https://files.pythonhosted.org/packages/d2/55/558cfe76b65d91d1854bbfac336020bd42fd887caa632d845d13c0c539eb/ty-0.0.61-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:08217382b3385808ee7288501ea3214b32631b08d1fd091ece6799b0c95264c5", size = 11602442, upload-time = "2026-07-18T01:39:20.914Z" }, + { url = "https://files.pythonhosted.org/packages/27/be/78c0ae6634cd606a68e5b46b338db427a48a1800c96a749b2d2f7a702e03/ty-0.0.61-py3-none-macosx_11_0_arm64.whl", hash = "sha256:6d99c729011b47dec20e78a32ac9c8f6defd4cf62f7bb851bbccf70dde6cee50", size = 11125286, upload-time = "2026-07-18T01:39:22.893Z" }, + { url = "https://files.pythonhosted.org/packages/a4/18/a40793962f1b6337938ddb0bca7496b54e70879e23b4d2cc8dfd7e5d1af3/ty-0.0.61-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5cda607978ae271b77e51c947663218bce635c3507e256865444b10c37cdb60d", size = 11663403, upload-time = "2026-07-18T01:39:25.017Z" }, + { url = "https://files.pythonhosted.org/packages/98/c1/7879244da5b30407dc368946d36be5024380073408b079f144ffe034030e/ty-0.0.61-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0d78f160a0f9434d570cdcdbc4dafba1f6aac3c47a32f9f63995b3cb55ffe4b6", size = 11715250, upload-time = "2026-07-18T01:39:27.045Z" }, + { url = "https://files.pythonhosted.org/packages/35/c4/8a4637cd58abd37f315dd515e24c582986cb1bfdf2edc4786882f5a4f69a/ty-0.0.61-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09aeab4800b36e93e4ce918699004da642d74988cac920b7592a6a2b9be6611c", size = 12393876, upload-time = "2026-07-18T01:39:29.197Z" }, + { url = "https://files.pythonhosted.org/packages/27/4b/27e7c640b1272743503229aa17ae2167a538040c4716a2fa1777c2b34fea/ty-0.0.61-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dccc8136df44142a109953a168be17b4915c99876b047d0b6672c31dae939bdf", size = 12958187, upload-time = "2026-07-18T01:39:31.308Z" }, + { url = "https://files.pythonhosted.org/packages/3a/f5/70eaaefb6081fb0a8115cff66fbfaa20dafac8c646df2477adad95a59de2/ty-0.0.61-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:220760c2d13a887d027ee1093172c24ac35b6e634805329c93a30908ae4d3f5c", size = 12560101, upload-time = "2026-07-18T01:39:33.35Z" }, + { url = "https://files.pythonhosted.org/packages/e3/5a/17bae3b6429b5c479dc6c1e344d34e1f79efbc27531f15f3ee5b5da63745/ty-0.0.61-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:effefbb89da7128d18059529d1c2ea390fe7f1f3882690d257ca2143d49a0c34", size = 12225389, upload-time = "2026-07-18T01:39:35.436Z" }, + { url = "https://files.pythonhosted.org/packages/d2/0e/2ac380ba20d6395542c8df1d6fa4f00e2aead784c2e6aaefa1e02ed0610c/ty-0.0.61-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:ba8b28a5ef811d5bb6461e37d76110c06fd20487474865c323d3d18b08b972b2", size = 12548403, upload-time = "2026-07-18T01:39:37.556Z" }, + { url = "https://files.pythonhosted.org/packages/b0/e5/7da4b73e825e1a9808c26d68b0156e9a37aede1846191210dfffb8c64042/ty-0.0.61-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:88ecd6d9b05e8174b1860dac9bd3e188d6cef5702b0d3239fd9f94f6ac73a29d", size = 11621813, upload-time = "2026-07-18T01:39:39.919Z" }, + { url = "https://files.pythonhosted.org/packages/9b/3c/5b58015e998cd0d89b17a463b6321421457d86d987574e8dac65ddfceba3/ty-0.0.61-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:fb0cdfe4c48542ffb9a1139825dfa3d4aae49e96e966682ef7da762ab97831ff", size = 11734101, upload-time = "2026-07-18T01:39:42.097Z" }, + { url = "https://files.pythonhosted.org/packages/a6/21/294f4cc819b7b12ed659fd860e5cdfbd592d4c768c8f23596685dbc43e6b/ty-0.0.61-py3-none-musllinux_1_2_i686.whl", hash = "sha256:dff03873c0c3d0b44738f8b6d403b0756a31cf54c65136397df7624c6159b1f0", size = 11988401, upload-time = "2026-07-18T01:39:44.183Z" }, + { url = "https://files.pythonhosted.org/packages/2e/26/0f96f79fdac118521a9771e9eef3f9b3f447d647b2c77953e80a1715c7e8/ty-0.0.61-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a9210e80e3d41c1dfc751e9e8e0980272f475031fafd0fb0f48aee233c78da03", size = 12330624, upload-time = "2026-07-18T01:39:46.662Z" }, + { url = "https://files.pythonhosted.org/packages/e3/08/1e62d1bca5c0cebdc7a34db1f4b61557aab85961cedd56953dd2c32d3e66/ty-0.0.61-py3-none-win32.whl", hash = "sha256:e3e1fe06f49a5492a922a5df2739834aa5ee978c7dd10414119dc8755cc40c9c", size = 11313991, upload-time = "2026-07-18T01:39:48.761Z" }, + { url = "https://files.pythonhosted.org/packages/26/f1/d8e33b3aeb36b73d81ae34d10e46ec4abf506d68f4e0a1491a76a593dd42/ty-0.0.61-py3-none-win_amd64.whl", hash = "sha256:25f2291169e0298fcdbba1b1fea64f8207a6c1908dddef32346fd5e3e6ac9221", size = 12311717, upload-time = "2026-07-18T01:39:50.881Z" }, + { url = "https://files.pythonhosted.org/packages/e1/14/7caec26d93a943c0e7d15eb7374644508d08cbd387d112b722b12d14e044/ty-0.0.61-py3-none-win_arm64.whl", hash = "sha256:3e496f7698bc4b5bbb1eb66d8b5799ba87596d88d36604ca359083893fa2fc49", size = 11693485, upload-time = "2026-07-18T01:39:52.73Z" }, ] [[package]] name = "typer" -version = "0.24.1" +version = "0.27.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-doc" }, - { name = "click" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, { name = "rich" }, { name = "shellingham" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f5/24/cb09efec5cc954f7f9b930bf8279447d24618bb6758d4f6adf2574c41780/typer-0.24.1.tar.gz", hash = "sha256:e39b4732d65fbdcde189ae76cf7cd48aeae72919dea1fdfc16593be016256b45", size = 118613, upload-time = "2026-02-21T16:54:40.609Z" } +sdist = { url = "https://files.pythonhosted.org/packages/37/78/fda3361b56efc27944f24225f6ecd13d96d6fcfe37bd0eb34e2f4c63f9fc/typer-0.27.0.tar.gz", hash = "sha256:629bd12ea5d13a17148125d9a264f949eb171fb3f120f9b04d85873cab054fa5", size = 203430, upload-time = "2026-07-15T19:21:07.007Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4a/91/48db081e7a63bb37284f9fbcefda7c44c277b18b0e13fbc36ea2335b71e6/typer-0.24.1-py3-none-any.whl", hash = "sha256:112c1f0ce578bfb4cab9ffdabc68f031416ebcc216536611ba21f04e9aa84c9e", size = 56085, upload-time = "2026-02-21T16:54:41.616Z" }, + { url = "https://files.pythonhosted.org/packages/40/03/26a383c9e58c213199d1aad1c3d353cfc22d4444ec6d2c0bf8ad02523843/typer-0.27.0-py3-none-any.whl", hash = "sha256:6f4b27631e47f077871b7dc30e933ec0131c1390fbe0e387ea5574b5bac9ccf1", size = 122716, upload-time = "2026-07-15T19:21:05.553Z" }, ] [[package]] name = "typing-extensions" -version = "4.15.0" +version = "4.16.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, + { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" }, ] [[package]] @@ -3266,20 +3380,20 @@ wheels = [ [[package]] name = "tzdata" -version = "2025.3" +version = "2026.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/5e/a7/c202b344c5ca7daf398f3b8a477eeb205cf3b6f32e7ec3a6bac0629ca975/tzdata-2025.3.tar.gz", hash = "sha256:de39c2ca5dc7b0344f2eba86f49d614019d29f060fc4ebc8a417896a620b56a7", size = 196772, upload-time = "2025-12-13T17:45:35.667Z" } +sdist = { url = "https://files.pythonhosted.org/packages/92/ff/5a28bdfd8c3ebec42564ac7d0e54ca3db65044a9314a97f9564fa7a1e926/tzdata-2026.3.tar.gz", hash = "sha256:4a1518b8993086a7982523e071643f3c0e5f213e75b21318e78bcabfff9d1415", size = 198674, upload-time = "2026-07-10T08:50:37.887Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1", size = 348521, upload-time = "2025-12-13T17:45:33.889Z" }, + { url = "https://files.pythonhosted.org/packages/e5/6d/b53b99a9f2766d095985947a5782f1702cabb129a34f7a802d7197af832f/tzdata-2026.3-py2.py3-none-any.whl", hash = "sha256:dc096730c87af6cab1b171c9d532be840741ff5d459015e7f6947bd7d7e54931", size = 348168, upload-time = "2026-07-10T08:50:36.46Z" }, ] [[package]] name = "uncalled-for" -version = "0.2.0" +version = "0.3.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/02/7c/b5b7d8136f872e3f13b0584e576886de0489d7213a12de6bebf29ff6ebfc/uncalled_for-0.2.0.tar.gz", hash = "sha256:b4f8fdbcec328c5a113807d653e041c5094473dd4afa7c34599ace69ccb7e69f", size = 49488, upload-time = "2026-02-27T17:40:58.137Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/82/345cc927f7fbdae6065e7768759932fcc827fc20b29b45dfbafa2f1f7da4/uncalled_for-0.3.2.tar.gz", hash = "sha256:89f5dbcd71e2b8f47c030b1fa302e6cce2ec795d1ac565eeb6525c5fe55cb8a2", size = 50032, upload-time = "2026-05-06T13:38:25.204Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ff/7f/4320d9ce3be404e6310b915c3629fe27bf1e2f438a1a7a3cb0396e32e9a9/uncalled_for-0.2.0-py3-none-any.whl", hash = "sha256:2c0bd338faff5f930918f79e7eb9ff48290df2cb05fcc0b40a7f334e55d4d85f", size = 11351, upload-time = "2026-02-27T17:40:56.804Z" }, + { url = "https://files.pythonhosted.org/packages/3b/25/2c87754f3a9e692315f7b811244090e68f362979fc8886b3fbd2985a1d8c/uncalled_for-0.3.2-py3-none-any.whl", hash = "sha256:0ff60b142c7d1f8070bde9d42afaa70aedc77dcc10998c227687e9c15713418e", size = 11444, upload-time = "2026-05-06T13:38:24.025Z" }, ] [[package]] @@ -3293,203 +3407,265 @@ wheels = [ [[package]] name = "uvicorn" -version = "0.42.0" +version = "0.51.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, { name = "h11" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e3/ad/4a96c425be6fb67e0621e62d86c402b4a17ab2be7f7c055d9bd2f638b9e2/uvicorn-0.42.0.tar.gz", hash = "sha256:9b1f190ce15a2dd22e7758651d9b6d12df09a13d51ba5bf4fc33c383a48e1775", size = 85393, upload-time = "2026-03-16T06:19:50.077Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/65/b7c6c443ccc58678c91e1e973bbe2a878591538655d6e1d47f24ba1c51f3/uvicorn-0.51.0.tar.gz", hash = "sha256:f6f4b69b657c312f516dd2d268ab9ae6f254b11e4bac504f37b2ab58b24dd0b0", size = 94412, upload-time = "2026-07-08T10:59:05.962Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0a/89/f8827ccff89c1586027a105e5630ff6139a64da2515e24dafe860bd9ae4d/uvicorn-0.42.0-py3-none-any.whl", hash = "sha256:96c30f5c7abe6f74ae8900a70e92b85ad6613b745d4879eb9b16ccad15645359", size = 68830, upload-time = "2026-03-16T06:19:48.325Z" }, + { url = "https://files.pythonhosted.org/packages/45/ec/dbb7e5a6b91f86bfb9eb7d2988a2730907b6a729875b949c7f022e8b88fa/uvicorn-0.51.0-py3-none-any.whl", hash = "sha256:5d38af6cd620f2ae3849fb44fd4879e0890aa1febe8d47eb355fb45d93fe6a5b", size = 73219, upload-time = "2026-07-08T10:59:04.44Z" }, ] [[package]] name = "watchfiles" -version = "1.1.1" +version = "1.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c2/c9/8869df9b2a2d6c59d79220a4db37679e74f807c559ffe5265e08b227a210/watchfiles-1.1.1.tar.gz", hash = "sha256:a173cb5c16c4f40ab19cecf48a534c409f7ea983ab8fed0741304a1c0a31b3f2", size = 94440, upload-time = "2025-10-14T15:06:21.08Z" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/41/5e1a4bb12aac5f1493fa1bdc11154eca3b258ca4eba65d39c473fe19d8e9/watchfiles-1.2.0.tar.gz", hash = "sha256:c995fba777f1ea992f090f9236e9284cf7a5d1a0130dd5a3d82c598cacd76838", size = 108252, upload-time = "2026-05-18T04:32:04.251Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a7/1a/206e8cf2dd86fddf939165a57b4df61607a1e0add2785f170a3f616b7d9f/watchfiles-1.1.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:eef58232d32daf2ac67f42dea51a2c80f0d03379075d44a587051e63cc2e368c", size = 407318, upload-time = "2025-10-14T15:04:18.753Z" }, - { url = "https://files.pythonhosted.org/packages/b3/0f/abaf5262b9c496b5dad4ed3c0e799cbecb1f8ea512ecb6ddd46646a9fca3/watchfiles-1.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:03fa0f5237118a0c5e496185cafa92878568b652a2e9a9382a5151b1a0380a43", size = 394478, upload-time = "2025-10-14T15:04:20.297Z" }, - { url = "https://files.pythonhosted.org/packages/b1/04/9cc0ba88697b34b755371f5ace8d3a4d9a15719c07bdc7bd13d7d8c6a341/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8ca65483439f9c791897f7db49202301deb6e15fe9f8fe2fed555bf986d10c31", size = 449894, upload-time = "2025-10-14T15:04:21.527Z" }, - { url = "https://files.pythonhosted.org/packages/d2/9c/eda4615863cd8621e89aed4df680d8c3ec3da6a4cf1da113c17decd87c7f/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f0ab1c1af0cb38e3f598244c17919fb1a84d1629cc08355b0074b6d7f53138ac", size = 459065, upload-time = "2025-10-14T15:04:22.795Z" }, - { url = "https://files.pythonhosted.org/packages/84/13/f28b3f340157d03cbc8197629bc109d1098764abe1e60874622a0be5c112/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3bc570d6c01c206c46deb6e935a260be44f186a2f05179f52f7fcd2be086a94d", size = 488377, upload-time = "2025-10-14T15:04:24.138Z" }, - { url = "https://files.pythonhosted.org/packages/86/93/cfa597fa9389e122488f7ffdbd6db505b3b915ca7435ecd7542e855898c2/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e84087b432b6ac94778de547e08611266f1f8ffad28c0ee4c82e028b0fc5966d", size = 595837, upload-time = "2025-10-14T15:04:25.057Z" }, - { url = "https://files.pythonhosted.org/packages/57/1e/68c1ed5652b48d89fc24d6af905d88ee4f82fa8bc491e2666004e307ded1/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:620bae625f4cb18427b1bb1a2d9426dc0dd5a5ba74c7c2cdb9de405f7b129863", size = 473456, upload-time = "2025-10-14T15:04:26.497Z" }, - { url = "https://files.pythonhosted.org/packages/d5/dc/1a680b7458ffa3b14bb64878112aefc8f2e4f73c5af763cbf0bd43100658/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:544364b2b51a9b0c7000a4b4b02f90e9423d97fbbf7e06689236443ebcad81ab", size = 455614, upload-time = "2025-10-14T15:04:27.539Z" }, - { url = "https://files.pythonhosted.org/packages/61/a5/3d782a666512e01eaa6541a72ebac1d3aae191ff4a31274a66b8dd85760c/watchfiles-1.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:bbe1ef33d45bc71cf21364df962af171f96ecaeca06bd9e3d0b583efb12aec82", size = 630690, upload-time = "2025-10-14T15:04:28.495Z" }, - { url = "https://files.pythonhosted.org/packages/9b/73/bb5f38590e34687b2a9c47a244aa4dd50c56a825969c92c9c5fc7387cea1/watchfiles-1.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1a0bb430adb19ef49389e1ad368450193a90038b5b752f4ac089ec6942c4dff4", size = 622459, upload-time = "2025-10-14T15:04:29.491Z" }, - { url = "https://files.pythonhosted.org/packages/f1/ac/c9bb0ec696e07a20bd58af5399aeadaef195fb2c73d26baf55180fe4a942/watchfiles-1.1.1-cp310-cp310-win32.whl", hash = "sha256:3f6d37644155fb5beca5378feb8c1708d5783145f2a0f1c4d5a061a210254844", size = 272663, upload-time = "2025-10-14T15:04:30.435Z" }, - { url = "https://files.pythonhosted.org/packages/11/a0/a60c5a7c2ec59fa062d9a9c61d02e3b6abd94d32aac2d8344c4bdd033326/watchfiles-1.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:a36d8efe0f290835fd0f33da35042a1bb5dc0e83cbc092dcf69bce442579e88e", size = 287453, upload-time = "2025-10-14T15:04:31.53Z" }, - { url = "https://files.pythonhosted.org/packages/1f/f8/2c5f479fb531ce2f0564eda479faecf253d886b1ab3630a39b7bf7362d46/watchfiles-1.1.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:f57b396167a2565a4e8b5e56a5a1c537571733992b226f4f1197d79e94cf0ae5", size = 406529, upload-time = "2025-10-14T15:04:32.899Z" }, - { url = "https://files.pythonhosted.org/packages/fe/cd/f515660b1f32f65df671ddf6f85bfaca621aee177712874dc30a97397977/watchfiles-1.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:421e29339983e1bebc281fab40d812742268ad057db4aee8c4d2bce0af43b741", size = 394384, upload-time = "2025-10-14T15:04:33.761Z" }, - { url = "https://files.pythonhosted.org/packages/7b/c3/28b7dc99733eab43fca2d10f55c86e03bd6ab11ca31b802abac26b23d161/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e43d39a741e972bab5d8100b5cdacf69db64e34eb19b6e9af162bccf63c5cc6", size = 448789, upload-time = "2025-10-14T15:04:34.679Z" }, - { url = "https://files.pythonhosted.org/packages/4a/24/33e71113b320030011c8e4316ccca04194bf0cbbaeee207f00cbc7d6b9f5/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f537afb3276d12814082a2e9b242bdcf416c2e8fd9f799a737990a1dbe906e5b", size = 460521, upload-time = "2025-10-14T15:04:35.963Z" }, - { url = "https://files.pythonhosted.org/packages/f4/c3/3c9a55f255aa57b91579ae9e98c88704955fa9dac3e5614fb378291155df/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b2cd9e04277e756a2e2d2543d65d1e2166d6fd4c9b183f8808634fda23f17b14", size = 488722, upload-time = "2025-10-14T15:04:37.091Z" }, - { url = "https://files.pythonhosted.org/packages/49/36/506447b73eb46c120169dc1717fe2eff07c234bb3232a7200b5f5bd816e9/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5f3f58818dc0b07f7d9aa7fe9eb1037aecb9700e63e1f6acfed13e9fef648f5d", size = 596088, upload-time = "2025-10-14T15:04:38.39Z" }, - { url = "https://files.pythonhosted.org/packages/82/ab/5f39e752a9838ec4d52e9b87c1e80f1ee3ccdbe92e183c15b6577ab9de16/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9bb9f66367023ae783551042d31b1d7fd422e8289eedd91f26754a66f44d5cff", size = 472923, upload-time = "2025-10-14T15:04:39.666Z" }, - { url = "https://files.pythonhosted.org/packages/af/b9/a419292f05e302dea372fa7e6fda5178a92998411f8581b9830d28fb9edb/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aebfd0861a83e6c3d1110b78ad54704486555246e542be3e2bb94195eabb2606", size = 456080, upload-time = "2025-10-14T15:04:40.643Z" }, - { url = "https://files.pythonhosted.org/packages/b0/c3/d5932fd62bde1a30c36e10c409dc5d54506726f08cb3e1d8d0ba5e2bc8db/watchfiles-1.1.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:5fac835b4ab3c6487b5dbad78c4b3724e26bcc468e886f8ba8cc4306f68f6701", size = 629432, upload-time = "2025-10-14T15:04:41.789Z" }, - { url = "https://files.pythonhosted.org/packages/f7/77/16bddd9779fafb795f1a94319dc965209c5641db5bf1edbbccace6d1b3c0/watchfiles-1.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:399600947b170270e80134ac854e21b3ccdefa11a9529a3decc1327088180f10", size = 623046, upload-time = "2025-10-14T15:04:42.718Z" }, - { url = "https://files.pythonhosted.org/packages/46/ef/f2ecb9a0f342b4bfad13a2787155c6ee7ce792140eac63a34676a2feeef2/watchfiles-1.1.1-cp311-cp311-win32.whl", hash = "sha256:de6da501c883f58ad50db3a32ad397b09ad29865b5f26f64c24d3e3281685849", size = 271473, upload-time = "2025-10-14T15:04:43.624Z" }, - { url = "https://files.pythonhosted.org/packages/94/bc/f42d71125f19731ea435c3948cad148d31a64fccde3867e5ba4edee901f9/watchfiles-1.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:35c53bd62a0b885bf653ebf6b700d1bf05debb78ad9292cf2a942b23513dc4c4", size = 287598, upload-time = "2025-10-14T15:04:44.516Z" }, - { url = "https://files.pythonhosted.org/packages/57/c9/a30f897351f95bbbfb6abcadafbaca711ce1162f4db95fc908c98a9165f3/watchfiles-1.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:57ca5281a8b5e27593cb7d82c2ac927ad88a96ed406aa446f6344e4328208e9e", size = 277210, upload-time = "2025-10-14T15:04:45.883Z" }, - { url = "https://files.pythonhosted.org/packages/74/d5/f039e7e3c639d9b1d09b07ea412a6806d38123f0508e5f9b48a87b0a76cc/watchfiles-1.1.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:8c89f9f2f740a6b7dcc753140dd5e1ab9215966f7a3530d0c0705c83b401bd7d", size = 404745, upload-time = "2025-10-14T15:04:46.731Z" }, - { url = "https://files.pythonhosted.org/packages/a5/96/a881a13aa1349827490dab2d363c8039527060cfcc2c92cc6d13d1b1049e/watchfiles-1.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bd404be08018c37350f0d6e34676bd1e2889990117a2b90070b3007f172d0610", size = 391769, upload-time = "2025-10-14T15:04:48.003Z" }, - { url = "https://files.pythonhosted.org/packages/4b/5b/d3b460364aeb8da471c1989238ea0e56bec24b6042a68046adf3d9ddb01c/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8526e8f916bb5b9a0a777c8317c23ce65de259422bba5b31325a6fa6029d33af", size = 449374, upload-time = "2025-10-14T15:04:49.179Z" }, - { url = "https://files.pythonhosted.org/packages/b9/44/5769cb62d4ed055cb17417c0a109a92f007114a4e07f30812a73a4efdb11/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2edc3553362b1c38d9f06242416a5d8e9fe235c204a4072e988ce2e5bb1f69f6", size = 459485, upload-time = "2025-10-14T15:04:50.155Z" }, - { url = "https://files.pythonhosted.org/packages/19/0c/286b6301ded2eccd4ffd0041a1b726afda999926cf720aab63adb68a1e36/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30f7da3fb3f2844259cba4720c3fc7138eb0f7b659c38f3bfa65084c7fc7abce", size = 488813, upload-time = "2025-10-14T15:04:51.059Z" }, - { url = "https://files.pythonhosted.org/packages/c7/2b/8530ed41112dd4a22f4dcfdb5ccf6a1baad1ff6eed8dc5a5f09e7e8c41c7/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8979280bdafff686ba5e4d8f97840f929a87ed9cdf133cbbd42f7766774d2aa", size = 594816, upload-time = "2025-10-14T15:04:52.031Z" }, - { url = "https://files.pythonhosted.org/packages/ce/d2/f5f9fb49489f184f18470d4f99f4e862a4b3e9ac2865688eb2099e3d837a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dcc5c24523771db3a294c77d94771abcfcb82a0e0ee8efd910c37c59ec1b31bb", size = 475186, upload-time = "2025-10-14T15:04:53.064Z" }, - { url = "https://files.pythonhosted.org/packages/cf/68/5707da262a119fb06fbe214d82dd1fe4a6f4af32d2d14de368d0349eb52a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1db5d7ae38ff20153d542460752ff397fcf5c96090c1230803713cf3147a6803", size = 456812, upload-time = "2025-10-14T15:04:55.174Z" }, - { url = "https://files.pythonhosted.org/packages/66/ab/3cbb8756323e8f9b6f9acb9ef4ec26d42b2109bce830cc1f3468df20511d/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:28475ddbde92df1874b6c5c8aaeb24ad5be47a11f87cde5a28ef3835932e3e94", size = 630196, upload-time = "2025-10-14T15:04:56.22Z" }, - { url = "https://files.pythonhosted.org/packages/78/46/7152ec29b8335f80167928944a94955015a345440f524d2dfe63fc2f437b/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:36193ed342f5b9842edd3532729a2ad55c4160ffcfa3700e0d54be496b70dd43", size = 622657, upload-time = "2025-10-14T15:04:57.521Z" }, - { url = "https://files.pythonhosted.org/packages/0a/bf/95895e78dd75efe9a7f31733607f384b42eb5feb54bd2eb6ed57cc2e94f4/watchfiles-1.1.1-cp312-cp312-win32.whl", hash = "sha256:859e43a1951717cc8de7f4c77674a6d389b106361585951d9e69572823f311d9", size = 272042, upload-time = "2025-10-14T15:04:59.046Z" }, - { url = "https://files.pythonhosted.org/packages/87/0a/90eb755f568de2688cb220171c4191df932232c20946966c27a59c400850/watchfiles-1.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:91d4c9a823a8c987cce8fa2690923b069966dabb196dd8d137ea2cede885fde9", size = 288410, upload-time = "2025-10-14T15:05:00.081Z" }, - { url = "https://files.pythonhosted.org/packages/36/76/f322701530586922fbd6723c4f91ace21364924822a8772c549483abed13/watchfiles-1.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:a625815d4a2bdca61953dbba5a39d60164451ef34c88d751f6c368c3ea73d404", size = 278209, upload-time = "2025-10-14T15:05:01.168Z" }, - { url = "https://files.pythonhosted.org/packages/bb/f4/f750b29225fe77139f7ae5de89d4949f5a99f934c65a1f1c0b248f26f747/watchfiles-1.1.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:130e4876309e8686a5e37dba7d5e9bc77e6ed908266996ca26572437a5271e18", size = 404321, upload-time = "2025-10-14T15:05:02.063Z" }, - { url = "https://files.pythonhosted.org/packages/2b/f9/f07a295cde762644aa4c4bb0f88921d2d141af45e735b965fb2e87858328/watchfiles-1.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5f3bde70f157f84ece3765b42b4a52c6ac1a50334903c6eaf765362f6ccca88a", size = 391783, upload-time = "2025-10-14T15:05:03.052Z" }, - { url = "https://files.pythonhosted.org/packages/bc/11/fc2502457e0bea39a5c958d86d2cb69e407a4d00b85735ca724bfa6e0d1a/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14e0b1fe858430fc0251737ef3824c54027bedb8c37c38114488b8e131cf8219", size = 449279, upload-time = "2025-10-14T15:05:04.004Z" }, - { url = "https://files.pythonhosted.org/packages/e3/1f/d66bc15ea0b728df3ed96a539c777acfcad0eb78555ad9efcaa1274688f0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f27db948078f3823a6bb3b465180db8ebecf26dd5dae6f6180bd87383b6b4428", size = 459405, upload-time = "2025-10-14T15:05:04.942Z" }, - { url = "https://files.pythonhosted.org/packages/be/90/9f4a65c0aec3ccf032703e6db02d89a157462fbb2cf20dd415128251cac0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:059098c3a429f62fc98e8ec62b982230ef2c8df68c79e826e37b895bc359a9c0", size = 488976, upload-time = "2025-10-14T15:05:05.905Z" }, - { url = "https://files.pythonhosted.org/packages/37/57/ee347af605d867f712be7029bb94c8c071732a4b44792e3176fa3c612d39/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfb5862016acc9b869bb57284e6cb35fdf8e22fe59f7548858e2f971d045f150", size = 595506, upload-time = "2025-10-14T15:05:06.906Z" }, - { url = "https://files.pythonhosted.org/packages/a8/78/cc5ab0b86c122047f75e8fc471c67a04dee395daf847d3e59381996c8707/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:319b27255aacd9923b8a276bb14d21a5f7ff82564c744235fc5eae58d95422ae", size = 474936, upload-time = "2025-10-14T15:05:07.906Z" }, - { url = "https://files.pythonhosted.org/packages/62/da/def65b170a3815af7bd40a3e7010bf6ab53089ef1b75d05dd5385b87cf08/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c755367e51db90e75b19454b680903631d41f9e3607fbd941d296a020c2d752d", size = 456147, upload-time = "2025-10-14T15:05:09.138Z" }, - { url = "https://files.pythonhosted.org/packages/57/99/da6573ba71166e82d288d4df0839128004c67d2778d3b566c138695f5c0b/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c22c776292a23bfc7237a98f791b9ad3144b02116ff10d820829ce62dff46d0b", size = 630007, upload-time = "2025-10-14T15:05:10.117Z" }, - { url = "https://files.pythonhosted.org/packages/a8/51/7439c4dd39511368849eb1e53279cd3454b4a4dbace80bab88feeb83c6b5/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:3a476189be23c3686bc2f4321dd501cb329c0a0469e77b7b534ee10129ae6374", size = 622280, upload-time = "2025-10-14T15:05:11.146Z" }, - { url = "https://files.pythonhosted.org/packages/95/9c/8ed97d4bba5db6fdcdb2b298d3898f2dd5c20f6b73aee04eabe56c59677e/watchfiles-1.1.1-cp313-cp313-win32.whl", hash = "sha256:bf0a91bfb5574a2f7fc223cf95eeea79abfefa404bf1ea5e339c0c1560ae99a0", size = 272056, upload-time = "2025-10-14T15:05:12.156Z" }, - { url = "https://files.pythonhosted.org/packages/1f/f3/c14e28429f744a260d8ceae18bf58c1d5fa56b50d006a7a9f80e1882cb0d/watchfiles-1.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:52e06553899e11e8074503c8e716d574adeeb7e68913115c4b3653c53f9bae42", size = 288162, upload-time = "2025-10-14T15:05:13.208Z" }, - { url = "https://files.pythonhosted.org/packages/dc/61/fe0e56c40d5cd29523e398d31153218718c5786b5e636d9ae8ae79453d27/watchfiles-1.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:ac3cc5759570cd02662b15fbcd9d917f7ecd47efe0d6b40474eafd246f91ea18", size = 277909, upload-time = "2025-10-14T15:05:14.49Z" }, - { url = "https://files.pythonhosted.org/packages/79/42/e0a7d749626f1e28c7108a99fb9bf524b501bbbeb9b261ceecde644d5a07/watchfiles-1.1.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:563b116874a9a7ce6f96f87cd0b94f7faf92d08d0021e837796f0a14318ef8da", size = 403389, upload-time = "2025-10-14T15:05:15.777Z" }, - { url = "https://files.pythonhosted.org/packages/15/49/08732f90ce0fbbc13913f9f215c689cfc9ced345fb1bcd8829a50007cc8d/watchfiles-1.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3ad9fe1dae4ab4212d8c91e80b832425e24f421703b5a42ef2e4a1e215aff051", size = 389964, upload-time = "2025-10-14T15:05:16.85Z" }, - { url = "https://files.pythonhosted.org/packages/27/0d/7c315d4bd5f2538910491a0393c56bf70d333d51bc5b34bee8e68e8cea19/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce70f96a46b894b36eba678f153f052967a0d06d5b5a19b336ab0dbbd029f73e", size = 448114, upload-time = "2025-10-14T15:05:17.876Z" }, - { url = "https://files.pythonhosted.org/packages/c3/24/9e096de47a4d11bc4df41e9d1e61776393eac4cb6eb11b3e23315b78b2cc/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cb467c999c2eff23a6417e58d75e5828716f42ed8289fe6b77a7e5a91036ca70", size = 460264, upload-time = "2025-10-14T15:05:18.962Z" }, - { url = "https://files.pythonhosted.org/packages/cc/0f/e8dea6375f1d3ba5fcb0b3583e2b493e77379834c74fd5a22d66d85d6540/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:836398932192dae4146c8f6f737d74baeac8b70ce14831a239bdb1ca882fc261", size = 487877, upload-time = "2025-10-14T15:05:20.094Z" }, - { url = "https://files.pythonhosted.org/packages/ac/5b/df24cfc6424a12deb41503b64d42fbea6b8cb357ec62ca84a5a3476f654a/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:743185e7372b7bc7c389e1badcc606931a827112fbbd37f14c537320fca08620", size = 595176, upload-time = "2025-10-14T15:05:21.134Z" }, - { url = "https://files.pythonhosted.org/packages/8f/b5/853b6757f7347de4e9b37e8cc3289283fb983cba1ab4d2d7144694871d9c/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:afaeff7696e0ad9f02cbb8f56365ff4686ab205fcf9c4c5b6fdfaaa16549dd04", size = 473577, upload-time = "2025-10-14T15:05:22.306Z" }, - { url = "https://files.pythonhosted.org/packages/e1/f7/0a4467be0a56e80447c8529c9fce5b38eab4f513cb3d9bf82e7392a5696b/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f7eb7da0eb23aa2ba036d4f616d46906013a68caf61b7fdbe42fc8b25132e77", size = 455425, upload-time = "2025-10-14T15:05:23.348Z" }, - { url = "https://files.pythonhosted.org/packages/8e/e0/82583485ea00137ddf69bc84a2db88bd92ab4a6e3c405e5fb878ead8d0e7/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:831a62658609f0e5c64178211c942ace999517f5770fe9436be4c2faeba0c0ef", size = 628826, upload-time = "2025-10-14T15:05:24.398Z" }, - { url = "https://files.pythonhosted.org/packages/28/9a/a785356fccf9fae84c0cc90570f11702ae9571036fb25932f1242c82191c/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:f9a2ae5c91cecc9edd47e041a930490c31c3afb1f5e6d71de3dc671bfaca02bf", size = 622208, upload-time = "2025-10-14T15:05:25.45Z" }, - { url = "https://files.pythonhosted.org/packages/c3/f4/0872229324ef69b2c3edec35e84bd57a1289e7d3fe74588048ed8947a323/watchfiles-1.1.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:d1715143123baeeaeadec0528bb7441103979a1d5f6fd0e1f915383fea7ea6d5", size = 404315, upload-time = "2025-10-14T15:05:26.501Z" }, - { url = "https://files.pythonhosted.org/packages/7b/22/16d5331eaed1cb107b873f6ae1b69e9ced582fcf0c59a50cd84f403b1c32/watchfiles-1.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:39574d6370c4579d7f5d0ad940ce5b20db0e4117444e39b6d8f99db5676c52fd", size = 390869, upload-time = "2025-10-14T15:05:27.649Z" }, - { url = "https://files.pythonhosted.org/packages/b2/7e/5643bfff5acb6539b18483128fdc0ef2cccc94a5b8fbda130c823e8ed636/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7365b92c2e69ee952902e8f70f3ba6360d0d596d9299d55d7d386df84b6941fb", size = 449919, upload-time = "2025-10-14T15:05:28.701Z" }, - { url = "https://files.pythonhosted.org/packages/51/2e/c410993ba5025a9f9357c376f48976ef0e1b1aefb73b97a5ae01a5972755/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bfff9740c69c0e4ed32416f013f3c45e2ae42ccedd1167ef2d805c000b6c71a5", size = 460845, upload-time = "2025-10-14T15:05:30.064Z" }, - { url = "https://files.pythonhosted.org/packages/8e/a4/2df3b404469122e8680f0fcd06079317e48db58a2da2950fb45020947734/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b27cf2eb1dda37b2089e3907d8ea92922b673c0c427886d4edc6b94d8dfe5db3", size = 489027, upload-time = "2025-10-14T15:05:31.064Z" }, - { url = "https://files.pythonhosted.org/packages/ea/84/4587ba5b1f267167ee715b7f66e6382cca6938e0a4b870adad93e44747e6/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:526e86aced14a65a5b0ec50827c745597c782ff46b571dbfe46192ab9e0b3c33", size = 595615, upload-time = "2025-10-14T15:05:32.074Z" }, - { url = "https://files.pythonhosted.org/packages/6a/0f/c6988c91d06e93cd0bb3d4a808bcf32375ca1904609835c3031799e3ecae/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04e78dd0b6352db95507fd8cb46f39d185cf8c74e4cf1e4fbad1d3df96faf510", size = 474836, upload-time = "2025-10-14T15:05:33.209Z" }, - { url = "https://files.pythonhosted.org/packages/b4/36/ded8aebea91919485b7bbabbd14f5f359326cb5ec218cd67074d1e426d74/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c85794a4cfa094714fb9c08d4a218375b2b95b8ed1666e8677c349906246c05", size = 455099, upload-time = "2025-10-14T15:05:34.189Z" }, - { url = "https://files.pythonhosted.org/packages/98/e0/8c9bdba88af756a2fce230dd365fab2baf927ba42cd47521ee7498fd5211/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:74d5012b7630714b66be7b7b7a78855ef7ad58e8650c73afc4c076a1f480a8d6", size = 630626, upload-time = "2025-10-14T15:05:35.216Z" }, - { url = "https://files.pythonhosted.org/packages/2a/84/a95db05354bf2d19e438520d92a8ca475e578c647f78f53197f5a2f17aaf/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:8fbe85cb3201c7d380d3d0b90e63d520f15d6afe217165d7f98c9c649654db81", size = 622519, upload-time = "2025-10-14T15:05:36.259Z" }, - { url = "https://files.pythonhosted.org/packages/1d/ce/d8acdc8de545de995c339be67711e474c77d643555a9bb74a9334252bd55/watchfiles-1.1.1-cp314-cp314-win32.whl", hash = "sha256:3fa0b59c92278b5a7800d3ee7733da9d096d4aabcfabb9a928918bd276ef9b9b", size = 272078, upload-time = "2025-10-14T15:05:37.63Z" }, - { url = "https://files.pythonhosted.org/packages/c4/c9/a74487f72d0451524be827e8edec251da0cc1fcf111646a511ae752e1a3d/watchfiles-1.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:c2047d0b6cea13b3316bdbafbfa0c4228ae593d995030fda39089d36e64fc03a", size = 287664, upload-time = "2025-10-14T15:05:38.95Z" }, - { url = "https://files.pythonhosted.org/packages/df/b8/8ac000702cdd496cdce998c6f4ee0ca1f15977bba51bdf07d872ebdfc34c/watchfiles-1.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:842178b126593addc05acf6fce960d28bc5fae7afbaa2c6c1b3a7b9460e5be02", size = 277154, upload-time = "2025-10-14T15:05:39.954Z" }, - { url = "https://files.pythonhosted.org/packages/47/a8/e3af2184707c29f0f14b1963c0aace6529f9d1b8582d5b99f31bbf42f59e/watchfiles-1.1.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:88863fbbc1a7312972f1c511f202eb30866370ebb8493aef2812b9ff28156a21", size = 403820, upload-time = "2025-10-14T15:05:40.932Z" }, - { url = "https://files.pythonhosted.org/packages/c0/ec/e47e307c2f4bd75f9f9e8afbe3876679b18e1bcec449beca132a1c5ffb2d/watchfiles-1.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:55c7475190662e202c08c6c0f4d9e345a29367438cf8e8037f3155e10a88d5a5", size = 390510, upload-time = "2025-10-14T15:05:41.945Z" }, - { url = "https://files.pythonhosted.org/packages/d5/a0/ad235642118090f66e7b2f18fd5c42082418404a79205cdfca50b6309c13/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f53fa183d53a1d7a8852277c92b967ae99c2d4dcee2bfacff8868e6e30b15f7", size = 448408, upload-time = "2025-10-14T15:05:43.385Z" }, - { url = "https://files.pythonhosted.org/packages/df/85/97fa10fd5ff3332ae17e7e40e20784e419e28521549780869f1413742e9d/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6aae418a8b323732fa89721d86f39ec8f092fc2af67f4217a2b07fd3e93c6101", size = 458968, upload-time = "2025-10-14T15:05:44.404Z" }, - { url = "https://files.pythonhosted.org/packages/47/c2/9059c2e8966ea5ce678166617a7f75ecba6164375f3b288e50a40dc6d489/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f096076119da54a6080e8920cbdaac3dbee667eb91dcc5e5b78840b87415bd44", size = 488096, upload-time = "2025-10-14T15:05:45.398Z" }, - { url = "https://files.pythonhosted.org/packages/94/44/d90a9ec8ac309bc26db808a13e7bfc0e4e78b6fc051078a554e132e80160/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00485f441d183717038ed2e887a7c868154f216877653121068107b227a2f64c", size = 596040, upload-time = "2025-10-14T15:05:46.502Z" }, - { url = "https://files.pythonhosted.org/packages/95/68/4e3479b20ca305cfc561db3ed207a8a1c745ee32bf24f2026a129d0ddb6e/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a55f3e9e493158d7bfdb60a1165035f1cf7d320914e7b7ea83fe22c6023b58fc", size = 473847, upload-time = "2025-10-14T15:05:47.484Z" }, - { url = "https://files.pythonhosted.org/packages/4f/55/2af26693fd15165c4ff7857e38330e1b61ab8c37d15dc79118cdba115b7a/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c91ed27800188c2ae96d16e3149f199d62f86c7af5f5f4d2c61a3ed8cd3666c", size = 455072, upload-time = "2025-10-14T15:05:48.928Z" }, - { url = "https://files.pythonhosted.org/packages/66/1d/d0d200b10c9311ec25d2273f8aad8c3ef7cc7ea11808022501811208a750/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:311ff15a0bae3714ffb603e6ba6dbfba4065ab60865d15a6ec544133bdb21099", size = 629104, upload-time = "2025-10-14T15:05:49.908Z" }, - { url = "https://files.pythonhosted.org/packages/e3/bd/fa9bb053192491b3867ba07d2343d9f2252e00811567d30ae8d0f78136fe/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:a916a2932da8f8ab582f242c065f5c81bed3462849ca79ee357dd9551b0e9b01", size = 622112, upload-time = "2025-10-14T15:05:50.941Z" }, - { url = "https://files.pythonhosted.org/packages/ba/4c/a888c91e2e326872fa4705095d64acd8aa2fb9c1f7b9bd0588f33850516c/watchfiles-1.1.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:17ef139237dfced9da49fb7f2232c86ca9421f666d78c264c7ffca6601d154c3", size = 409611, upload-time = "2025-10-14T15:06:05.809Z" }, - { url = "https://files.pythonhosted.org/packages/1e/c7/5420d1943c8e3ce1a21c0a9330bcf7edafb6aa65d26b21dbb3267c9e8112/watchfiles-1.1.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:672b8adf25b1a0d35c96b5888b7b18699d27d4194bac8beeae75be4b7a3fc9b2", size = 396889, upload-time = "2025-10-14T15:06:07.035Z" }, - { url = "https://files.pythonhosted.org/packages/0c/e5/0072cef3804ce8d3aaddbfe7788aadff6b3d3f98a286fdbee9fd74ca59a7/watchfiles-1.1.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77a13aea58bc2b90173bc69f2a90de8e282648939a00a602e1dc4ee23e26b66d", size = 451616, upload-time = "2025-10-14T15:06:08.072Z" }, - { url = "https://files.pythonhosted.org/packages/83/4e/b87b71cbdfad81ad7e83358b3e447fedd281b880a03d64a760fe0a11fc2e/watchfiles-1.1.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b495de0bb386df6a12b18335a0285dda90260f51bdb505503c02bcd1ce27a8b", size = 458413, upload-time = "2025-10-14T15:06:09.209Z" }, - { url = "https://files.pythonhosted.org/packages/d3/8e/e500f8b0b77be4ff753ac94dc06b33d8f0d839377fee1b78e8c8d8f031bf/watchfiles-1.1.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:db476ab59b6765134de1d4fe96a1a9c96ddf091683599be0f26147ea1b2e4b88", size = 408250, upload-time = "2025-10-14T15:06:10.264Z" }, - { url = "https://files.pythonhosted.org/packages/bd/95/615e72cd27b85b61eec764a5ca51bd94d40b5adea5ff47567d9ebc4d275a/watchfiles-1.1.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:89eef07eee5e9d1fda06e38822ad167a044153457e6fd997f8a858ab7564a336", size = 396117, upload-time = "2025-10-14T15:06:11.28Z" }, - { url = "https://files.pythonhosted.org/packages/c9/81/e7fe958ce8a7fb5c73cc9fb07f5aeaf755e6aa72498c57d760af760c91f8/watchfiles-1.1.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce19e06cbda693e9e7686358af9cd6f5d61312ab8b00488bc36f5aabbaf77e24", size = 450493, upload-time = "2025-10-14T15:06:12.321Z" }, - { url = "https://files.pythonhosted.org/packages/6e/d4/ed38dd3b1767193de971e694aa544356e63353c33a85d948166b5ff58b9e/watchfiles-1.1.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e6f39af2eab0118338902798b5aa6664f46ff66bc0280de76fca67a7f262a49", size = 457546, upload-time = "2025-10-14T15:06:13.372Z" }, + { url = "https://files.pythonhosted.org/packages/0d/5a/2bf22ecb24916983bf1cc0095e7dea2741d14d6553b0d6a2ac8bc96eca93/watchfiles-1.2.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:bb68bf4df85abebe5efddc53cf2075520f243a59868d9b3973278b23e76962a9", size = 400471, upload-time = "2026-05-18T04:31:08.908Z" }, + { url = "https://files.pythonhosted.org/packages/55/70/dea1f6a0e76607841a60fb51af150e70124864673f61704abb62b90cdcc7/watchfiles-1.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c16cb06dd17d43b9d185094268459eac92c9538356f050e55b54e82cf700e1d4", size = 394599, upload-time = "2026-05-18T04:30:19.845Z" }, + { url = "https://files.pythonhosted.org/packages/18/52/752dcc7dc817baef5e89518732925795ce52e36a683a9a3c9fb68b21504e/watchfiles-1.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77a0feab9af4c021c581f695258c642b3d10c5fd4c676e33a0d8606425d82631", size = 455458, upload-time = "2026-05-18T04:30:29.126Z" }, + { url = "https://files.pythonhosted.org/packages/12/48/366ebbb22fcc504c2f72b45f0b7e72f40a18795cc01752c16066d597b67a/watchfiles-1.2.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a16ffe19bf5cf9f5edaa1ad1dd830c5a816e8feec430c522302ab55483a4b994", size = 460513, upload-time = "2026-05-18T04:31:40.85Z" }, + { url = "https://files.pythonhosted.org/packages/ad/44/1f9e1b15e7a729062e0d0c3d0d7225ea4ab98b2267ef87287153be2495fc/watchfiles-1.2.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:204f299afcbd65918ab78dbc52626b0ae45e9d8cef403fdbf33ecf9e40eac66e", size = 493616, upload-time = "2026-05-18T04:30:58.47Z" }, + { url = "https://files.pythonhosted.org/packages/7e/55/8b1086dcc8a1d6a697a62767bd7ea368e74c61c6fd171683cfe24a3fe5d2/watchfiles-1.2.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:11743adfa510bfffebe97659fb280182b5c9b238708f667e866f308c3430dc19", size = 573154, upload-time = "2026-05-18T04:30:37.903Z" }, + { url = "https://files.pythonhosted.org/packages/14/7a/242f400cc77fafa7b18d53d19d9cb64fc6a6f61f28c55913bae7c674d92a/watchfiles-1.2.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eb72919d93e3a16fc451d3aa3d4b1698423daca1b382d3d959c9ac51297c12a8", size = 467046, upload-time = "2026-05-18T04:30:41.869Z" }, + { url = "https://files.pythonhosted.org/packages/02/c8/79eee650c62d2c186598489814468e389b5def0ebe755399ff645b35b1b2/watchfiles-1.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62f042afde2dde21ec1d2c1a74361e804673df86f51e418a999c9acfe671b07", size = 457100, upload-time = "2026-05-18T04:31:13.064Z" }, + { url = "https://files.pythonhosted.org/packages/81/36/519f6dbb7a95e4fe7c1513ed25b1520295ef9905a27f1f2226a73892bfb7/watchfiles-1.2.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:027ae72bfdfd254862065d8b3e2a815c6ab9b1853ce41e6648ece84afd34a551", size = 467038, upload-time = "2026-05-18T04:30:32.915Z" }, + { url = "https://files.pythonhosted.org/packages/2f/12/951af6b9f89097e02511122258402cb3578443021930b70cf968d6310dc0/watchfiles-1.2.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e1cfd51e97e13ff3bd047c140764d277fc9b95b7cb5da59e46a47d167adab310", size = 632563, upload-time = "2026-05-18T04:30:11.539Z" }, + { url = "https://files.pythonhosted.org/packages/28/cc/0cba1f0a6117b7ec117271bdc3cb3a5a252005959755a2c09a745e0942cc/watchfiles-1.2.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:24b2405c0a46738dd9e1cf7135aa5dbdb9d42d024628651b3b13d5117e99f8df", size = 660851, upload-time = "2026-05-18T04:31:53.186Z" }, + { url = "https://files.pythonhosted.org/packages/d0/f2/26347558cc8bf6877845e66b315f644d03c173906aa09e233a3f4fd23928/watchfiles-1.2.0-cp310-cp310-win32.whl", hash = "sha256:8c520725602756229f045b032a1ff33d7ef0f7404189d62f6c2438cb6d8ef6a1", size = 277023, upload-time = "2026-05-18T04:30:18.825Z" }, + { url = "https://files.pythonhosted.org/packages/6d/68/a5e67b6b68e94f4c1511d61c46c55eba0737583620b6febf194c7b9cc23f/watchfiles-1.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:03b14855c6f35539e2d95c442ae9530a75762f1e26567152b9ed05f96534a74d", size = 290107, upload-time = "2026-05-18T04:32:09.677Z" }, + { url = "https://files.pythonhosted.org/packages/fc/3d/8024c801df84d1587740d0359e7fdd80afeae3d159011f3d5376dd82f18e/watchfiles-1.2.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:704fd259e332e01f9b9c178f4bce9e49027e5587cc2600eeeaf8e76e1c846201", size = 400242, upload-time = "2026-05-18T04:31:19.014Z" }, + { url = "https://files.pythonhosted.org/packages/87/5b/f4dfd45323e949984a3a7f9dc31d1cbb049921e7d98253488dda72ccdaa9/watchfiles-1.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6543cf55d170003296d185c0af981f3e1311564907e1f4e08671fc7693a890a5", size = 394562, upload-time = "2026-05-18T04:30:08.46Z" }, + { url = "https://files.pythonhosted.org/packages/98/d8/19483ef075d601c409bce8bcbb5c0f81a10876fff870400568f08ce484a1/watchfiles-1.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:89d8c2394a065ca86f5d2910ff263ae67c127e1376ccc4f9fc35c71db879f80a", size = 456611, upload-time = "2026-05-18T04:30:45.723Z" }, + { url = "https://files.pythonhosted.org/packages/b1/6a/cc81fbe7ee42f2f22e661a6e12def7807e01b14b2f39e0ff83fd373fd307/watchfiles-1.2.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:772b80df316480d894a0e3165fdd19cf77f5d17f9a787f94029465ad0e3529d1", size = 461379, upload-time = "2026-05-18T04:31:29.292Z" }, + { url = "https://files.pythonhosted.org/packages/b1/57/7e669002082c0a0f4fb5113bb70125f7110124b846b0a11bc5ae8e90eac1/watchfiles-1.2.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d158cd89df6053823533e06fb1d73c549133bff5f0396170c0e53d9559340717", size = 493556, upload-time = "2026-05-18T04:30:05.44Z" }, + { url = "https://files.pythonhosted.org/packages/45/7d/f60a2b19807b21fe8281f3a8da4f59eef0d5f96825ac4680ba2d4f2ebf91/watchfiles-1.2.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d516b3283a758e087841aedb8031549fb41ced08f3db10aa6d2bf32dc042525b", size = 575255, upload-time = "2026-05-18T04:30:40.568Z" }, + { url = "https://files.pythonhosted.org/packages/bd/49/77f5b5e6efbcd57482f74948ebb1b97e5c0046d6b61475042d830c84b3ff/watchfiles-1.2.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:53b2290c92e0506d102cd448fbc610d87079553f86caa39d67440856a8b8bba5", size = 467052, upload-time = "2026-05-18T04:31:17.942Z" }, + { url = "https://files.pythonhosted.org/packages/ee/5a/73e2959af1b97fd5d556f9a8bdba017be23ceeef731869d5eaa0a753d5a3/watchfiles-1.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a711b51aec4370d0dcda5b6c09463206f133a5759341d7744b953a7b62e1100e", size = 456858, upload-time = "2026-05-18T04:30:30.182Z" }, + { url = "https://files.pythonhosted.org/packages/50/57/1bc8c27fad7e6c19bddee15d276dbb6ab72480ec01c127afff1673aee417/watchfiles-1.2.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:e2ca07fa7d89195ec0865d3d285666286740bfa83d83e5cee204043a31ecc165", size = 467579, upload-time = "2026-05-18T04:32:15.897Z" }, + { url = "https://files.pythonhosted.org/packages/09/6c/3c2e44edba3553c5e3c3b8c8a2a6dee6b9e12ae2cf4bd2378bebf9dc3038/watchfiles-1.2.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e0618518f282c4ebff60f5e5b1247b6d91bb8b9f4476947563a1e74acc66f3c6", size = 633253, upload-time = "2026-05-18T04:31:37.123Z" }, + { url = "https://files.pythonhosted.org/packages/30/c2/d8c84a882ab39bbefcc4915ab3e91830b7a7e990c5570b0b69075aba3faf/watchfiles-1.2.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0d191c054d0715c3c95c99df9b8dbf6fd096d8c1e021e8f212e1bd8bc444ccb5", size = 660713, upload-time = "2026-05-18T04:31:24.62Z" }, + { url = "https://files.pythonhosted.org/packages/a9/07/f97736a5fc605364fe67b25e9fa4a6965dfd4840d50c406ada507e9d735f/watchfiles-1.2.0-cp311-cp311-win32.whl", hash = "sha256:9342472aff9b093c5acd4f6d8f70ae0937964ab56542502bcf5579782da69ae8", size = 277222, upload-time = "2026-05-18T04:31:21.131Z" }, + { url = "https://files.pythonhosted.org/packages/cf/99/2b04981977fc2608afd60360d928c6aecf6b950292ca221d98f4005f6694/watchfiles-1.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:dbd6c97045dad81227c8d040173da044c1de08de64a5ea8b555da4aee1d5fa22", size = 290274, upload-time = "2026-05-18T04:31:45.966Z" }, + { url = "https://files.pythonhosted.org/packages/3c/74/f7f58a7075ee9cf612b0cfcddb78b8cd8234f0742d6f0075cf0da2dde1c6/watchfiles-1.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:57a2d9fa4fb4c2ecae57b13dfff2c7ab53e21a2ba674fe9f05506680fcdcc0d7", size = 283460, upload-time = "2026-05-18T04:31:39.126Z" }, + { url = "https://files.pythonhosted.org/packages/b8/2f/e42c992d2afda3108ea1c02acecc991b9f31d05c14adc2a7cee9ee211fc4/watchfiles-1.2.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:bc13eb17538be00c874699dc0abe4ee2bc8d50bb1166a6b9e175ef3fd7eb8f26", size = 400115, upload-time = "2026-05-18T04:32:02.06Z" }, + { url = "https://files.pythonhosted.org/packages/5f/8f/6af2ea19065c91d8b0ea3516fdfc8c0d349f407e8e9fbf4e5a17360de8ad/watchfiles-1.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2d95ddc1eb6914154253d239089900813f6a767e174b8e6a50e7fdacb7e4236c", size = 393659, upload-time = "2026-05-18T04:30:50.951Z" }, + { url = "https://files.pythonhosted.org/packages/13/01/b32a967c56fb3e3e5be3db52c3d3b87fa4513aa367d8ed1ad96d42952e5f/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f70d8b291ef6e88d19b1f297a6905ddb978888d9272b0d05e6f53309856bcfc", size = 453207, upload-time = "2026-05-18T04:31:04.231Z" }, + { url = "https://files.pythonhosted.org/packages/04/98/97557a812180338cb1abd32e1cffcc4588f59b5f23e0cb006b2ba95ba64a/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:56d8641cf834c2836922899105bd3ce3d0dfc69291d52edf0b4d0436829b34c0", size = 459273, upload-time = "2026-05-18T04:31:50.377Z" }, + { url = "https://files.pythonhosted.org/packages/e8/a8/b4b08dcb7653b8087c6586f7ce649505900e866bbcfe40dc9587af02e686/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2581a94056e55d7d0a31a823ea92bf73749c489ca2285bfdc0fbe6b2bb49d50c", size = 489927, upload-time = "2026-05-18T04:31:42.485Z" }, + { url = "https://files.pythonhosted.org/packages/50/94/3dceea03545d2e5ddfd839f0ddd5e1cecbf1697b5a428d5ba11cef6af95d/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:41bc1199f7523b3f82843c88cbb979180c949caef0342cf90968f178e5d49b01", size = 570476, upload-time = "2026-05-18T04:31:03.071Z" }, + { url = "https://files.pythonhosted.org/packages/cc/f2/d39a5450c3532092b91f81d274360e613c2371bc874a89c7a1a3c5e8d138/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7571e4464cb6e434958f867f7f730b8ab0b75e3f8e5eac0499168486ab3c33a8", size = 465650, upload-time = "2026-05-18T04:30:12.701Z" }, + { url = "https://files.pythonhosted.org/packages/22/24/ed72f68cbc1333ca9b9f2200aa048bb6658ae41709bc1caad4310f4bdffd/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e53a384f76b631c3ae5334ce6a52f0baa3a911eb94a4eac7f160079868b716d5", size = 456398, upload-time = "2026-05-18T04:30:13.784Z" }, + { url = "https://files.pythonhosted.org/packages/0d/64/982ef4a4e5bab5b6e5b6becc8cd5e732f6130a78b855f0abec6439a9a135/watchfiles-1.2.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:d20029a60a71a052a24c4db7673bc4de39ab89adbaccbfb5d67987c5d73f424d", size = 465140, upload-time = "2026-05-18T04:31:52.111Z" }, + { url = "https://files.pythonhosted.org/packages/a0/0c/95282abf4ed680b6096010bcfc30c5fa7a041fc5aa5a2ad17a2cc6c75bba/watchfiles-1.2.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:2cb93af48550faf1cea04c303107c8b75833de7013e57ce27d3b8d21d8d0f58c", size = 630259, upload-time = "2026-05-18T04:31:25.676Z" }, + { url = "https://files.pythonhosted.org/packages/30/45/607c1de1530c4bdcf2cf1d1ecc2505ddba5d96bd43ba9f2b0e79876f850f/watchfiles-1.2.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2995c176de7692b86a2e4c58d9ec718f753150a979cb4a754e2b4ffa38e70906", size = 659859, upload-time = "2026-05-18T04:30:24.333Z" }, + { url = "https://files.pythonhosted.org/packages/fa/08/d9e2e0f9e8e6791d33aefc694ad7eefa7f901f63caff84a81ded38692f9c/watchfiles-1.2.0-cp312-cp312-win32.whl", hash = "sha256:7a2cffd17d27d2ecbb310c2b1d8174f222a5495b1a721894afa88ec11e25b898", size = 275480, upload-time = "2026-05-18T04:30:31.307Z" }, + { url = "https://files.pythonhosted.org/packages/1c/e6/9d42569c0102645cc8cea5d8c7d8a1e9d4ada2cb7f05f75e554b8aa2202a/watchfiles-1.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:f155b3a1b2a5fc89cdc70d47ee5d54e3b75e88efa34982028a35daef9ba00379", size = 288718, upload-time = "2026-05-18T04:32:10.745Z" }, + { url = "https://files.pythonhosted.org/packages/0a/26/88e0dc6ee3898169d7fa22bb6a69cabf2502d2ee25cb8c876d1262d204f8/watchfiles-1.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:8fa585ede612ee9f9e91b18bebf9ba11b9ae29a4e3a0d0cf6fca3e382133f0d5", size = 281026, upload-time = "2026-05-18T04:30:22.23Z" }, + { url = "https://files.pythonhosted.org/packages/d1/4d/70a7feced9f87e2ff26dba42667290f41694fc64646c67261fbb8cab5d5c/watchfiles-1.2.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:01ea8d66f0693b9b60a6541c8d10263091ca9a9060d242f3c1f3143f9aad2c98", size = 399730, upload-time = "2026-05-18T04:31:38.162Z" }, + { url = "https://files.pythonhosted.org/packages/31/3a/0da302f2307aee316922806ebd5726c542cbd787c938271cf14a074c7daf/watchfiles-1.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7ba0480b9a74af058f43b337e937a451e109295c420916d68ad24e3dc02f5e44", size = 392842, upload-time = "2026-05-18T04:30:27.051Z" }, + { url = "https://files.pythonhosted.org/packages/db/ef/d5bdb705c224dbc256aa0c1ec47bf4e61ec52558f2afb44a71a1fe4d7015/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f34e26a19f91f710c08e0183429f0d1d15df734e6bc78c31e77b9ea9c433658", size = 452989, upload-time = "2026-05-18T04:31:11.945Z" }, + { url = "https://files.pythonhosted.org/packages/71/29/5495f2c1661949ef7a35e4d71111d129cfe7606414a26887a919d0a55406/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b4e77f6a55f858504069abd35d336a637555c09bca453dde1ee1e5ada8a6a1fb", size = 458978, upload-time = "2026-05-18T04:30:52.606Z" }, + { url = "https://files.pythonhosted.org/packages/d5/8c/7f9c07c433811c2fffd93e13fdfb7135de9aab5f2ae41be08960fa0047dc/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0cb4d80e212f116474a545c21c912b445f16bb0cef9e6a73a498164223e14e2f", size = 490248, upload-time = "2026-05-18T04:31:36.003Z" }, + { url = "https://files.pythonhosted.org/packages/3c/11/d93632febc52fbc21be90231bb7c17fd5387f46c9076fd40a5f9c2ae6910/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b974946a10af379d425e2eef5b62f5c6ebeaccf91d45eaad6f5b27ecd4f91aa0", size = 571847, upload-time = "2026-05-18T04:31:10.862Z" }, + { url = "https://files.pythonhosted.org/packages/55/b4/383173e73aabb07ad1d9c7aa859d95437ac46a6d6a1e11005facda0c9d19/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:86bc13c25a8d1fcd70b51d0ce7c9b65e90de5666fcbfd3e34957cc73ee19aeb5", size = 465974, upload-time = "2026-05-18T04:30:17.006Z" }, + { url = "https://files.pythonhosted.org/packages/a7/6c/89b1a230a78f57c52dd8893adb1f92f94411721b6ec12596c56d98c74356/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca148d73dea36c9763aaa351e4d7a51780ec1584217c45276f4fe8239c768b71", size = 454782, upload-time = "2026-05-18T04:30:35.656Z" }, + { url = "https://files.pythonhosted.org/packages/24/62/1732118367cfff0a9fce3bf62ff4bfded09ef5df21d9d446b858b3f70a96/watchfiles-1.2.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:c525543d91961c6955b2636b308569e84a1d1c5f5f2932041ab9ef46422f43e3", size = 465182, upload-time = "2026-05-18T04:30:20.846Z" }, + { url = "https://files.pythonhosted.org/packages/28/96/716f7e5f51339bf22963f3345f9f27d7f3b30e2eadc597e257c881dd3c53/watchfiles-1.2.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:a204794696ffb8f9b10fba6f7cb5216d42f3b2b71860ccac6b6e42f5f10973b0", size = 629841, upload-time = "2026-05-18T04:31:05.397Z" }, + { url = "https://files.pythonhosted.org/packages/4c/fe/c40783950fd771ccf66ab3ec2722d188a9af1c7f96c6e811f36e40c6e03f/watchfiles-1.2.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:10d86db20695afe7997ac9e1717637d6714a8d0220458c33f3d2061f54cec427", size = 658028, upload-time = "2026-05-18T04:31:48.22Z" }, + { url = "https://files.pythonhosted.org/packages/71/72/4508db1856d1d87fcbb3b63f4839bab1b5682cb0e8d224d122263c09654a/watchfiles-1.2.0-cp313-cp313-win32.whl", hash = "sha256:eb283ee99e21ad6443c8cdb06ac5b34b1308c329cbdf03fa02b445363714c799", size = 275183, upload-time = "2026-05-18T04:30:59.57Z" }, + { url = "https://files.pythonhosted.org/packages/f9/36/14b76ca57652e5cc5fd1c11f32a261292c08a0d19a00351013c2549cbfb2/watchfiles-1.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:a0f27f01bee51861392bb6b7c4fdb290b27d1eb194e9e28788d68102a0e898d9", size = 288059, upload-time = "2026-05-18T04:32:07.937Z" }, + { url = "https://files.pythonhosted.org/packages/1b/8d/0a85e395398d8d20fadfe5c5d32c726eee17a519e78fb356f2cf7531bffe/watchfiles-1.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:3651aa7058595e9cfb75d35dd5ada2bf9f48a5b8a0f3562821d3e210c507e077", size = 280186, upload-time = "2026-05-18T04:31:54.484Z" }, + { url = "https://files.pythonhosted.org/packages/37/68/36db056f1fdcc5f07302f56e631774d6835bcd6fa3ace402304621d5f9e5/watchfiles-1.2.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:faea288b6f0ab1902ef08f4ca6de005dccf856c4e0c4f21b8c5fce02d90a1b08", size = 399031, upload-time = "2026-05-18T04:30:44.576Z" }, + { url = "https://files.pythonhosted.org/packages/c1/64/01a9d6f66a82a5c101ce939274106cc72759d62427e153f01edd2b9f87c2/watchfiles-1.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:01859b11fd9fbca670f4d5da00fbac282cfea9bd67a2125d8b2833a3b5617ea9", size = 391205, upload-time = "2026-05-18T04:30:25.413Z" }, + { url = "https://files.pythonhosted.org/packages/84/2c/0a44fe058cb4bb7b8ede6b6670698bbb7c0400740e378d00022189b7b31d/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fff610d7bb2256a317bb1e96f0d7862c7aa8076733ee5df0fd41bbe76a24a4f4", size = 451892, upload-time = "2026-05-18T04:32:14.005Z" }, + { url = "https://files.pythonhosted.org/packages/67/a1/351e0d56cd35e6488b5c8b4fb11a809a5bc923e8fe8fed9faf8920be0c89/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b141a4891c995a039cd89e9a49e62df1dc8a559a5d1a6e4c7106d16c12777a55", size = 458867, upload-time = "2026-05-18T04:31:22.279Z" }, + { url = "https://files.pythonhosted.org/packages/d5/7d/9d09605187f1b838998624049fcf8bf47b73c1a3b76901fcac1782f62277/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f22943b7770483f6ea0721c6b11d022947a98eb0acae14694de034f4d0d38925", size = 490217, upload-time = "2026-05-18T04:31:43.657Z" }, + { url = "https://files.pythonhosted.org/packages/60/5d/a17a16eccb182f04188cd308ec24b1a71a9b5c4e7098269cf35d9fa56d02/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1bc6195825b7dcd217968bb1f801a60fd4c16e8eeab5bedc7fe917d7d5995ab4", size = 571458, upload-time = "2026-05-18T04:32:11.875Z" }, + { url = "https://files.pythonhosted.org/packages/d3/3d/4dd457062083ab1938e5dfd45032eb425cee2ac817287ca8ff4356183e5d/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d4a4b147f5dca2a5d325a06a832fb43f345751adfbc63204aec30e0d9ca965a2", size = 464707, upload-time = "2026-05-18T04:30:43.492Z" }, + { url = "https://files.pythonhosted.org/packages/c6/71/ea8c57b128f5383de74d0c7d2d9c57ad7c9a65a930c451bd25d524b295b7/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4543579a9bdb0c9560039b4ffddbdb39545707659fbc430ce4c10f3f68d557f9", size = 454663, upload-time = "2026-05-18T04:30:16.061Z" }, + { url = "https://files.pythonhosted.org/packages/53/fd/2e812bf938406d7db351f0703ddd3fc6c061cf30d96153a77bc79a943a44/watchfiles-1.2.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:20aa0e708b920bde876a4aa82dc7dd6ebea228a63a67cda6632c2fc87b787efa", size = 463537, upload-time = "2026-05-18T04:31:44.9Z" }, + { url = "https://files.pythonhosted.org/packages/86/56/d17a7f1dd1bc3035f1072694a551301272f1739c2d8e319c927cb9e29b38/watchfiles-1.2.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:d413349d565dab74297f2a63e84a097936be69bf8f3b3801f27f380e32040f44", size = 629194, upload-time = "2026-05-18T04:31:14.141Z" }, + { url = "https://files.pythonhosted.org/packages/be/06/f1ff66bf5cae50aa4062779a0ecd0bbaf15e466195719074078947d9a17d/watchfiles-1.2.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:f28b2725eb8cce327b9b3ab02415c853011dc55c95832fe90de6bc56f5315f72", size = 656194, upload-time = "2026-05-18T04:31:47.14Z" }, + { url = "https://files.pythonhosted.org/packages/e7/54/a9c7ea9a82a4ac65e7004c0a03920b5cdd2f9c3b678757d9cd425aa51d53/watchfiles-1.2.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:b8c8358484d5fa12ef34f05b7f4168eaf1932f408725ff6d023c33ec17bd79d4", size = 400205, upload-time = "2026-05-18T04:32:05.153Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5d/c9ab3534374a4a67450696905d6ef16a04405448b8dc52bd752ae50423d4/watchfiles-1.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9f04b092229ad2c50126dd3c922c8822e51e605993764a33058d4a791ab42281", size = 392508, upload-time = "2026-05-18T04:30:54.849Z" }, + { url = "https://files.pythonhosted.org/packages/26/ca/1ad30103535cf0cecd7b993e8d50edc5351b1820e38f2d22e3df58962feb/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7a7ce236284f002a156f70add88efe5c70879cccbb658be0822c54b1306fc09d", size = 452448, upload-time = "2026-05-18T04:30:53.727Z" }, + { url = "https://files.pythonhosted.org/packages/37/a1/ceee2cdf2afbd715fa07758d39c9859513eae411b23196f7fd039e5feedd/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b9909cc2b48468b575eefa944919e1fe8a36c5849d5c7c168f80a8c1db69398e", size = 459605, upload-time = "2026-05-18T04:30:23.312Z" }, + { url = "https://files.pythonhosted.org/packages/e8/f6/421e30fd1cb3907a84ed92ab3f1983e37ba2dca015e9a894a048418417a2/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a37faaed405c67e28e6be45a1fa4f206ef5a2860f27c237db9fa30704c38242", size = 490757, upload-time = "2026-05-18T04:30:47.358Z" }, + { url = "https://files.pythonhosted.org/packages/41/b0/55ed1b97ed08be7bba6f9a541cac15f2a858e1d74d2b07b6da70a82aab00/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9649193aa27bd9ff2e80ff29bfaa93085496c7a3a377592823cc58b77ee88add", size = 568672, upload-time = "2026-05-18T04:30:38.915Z" }, + { url = "https://files.pythonhosted.org/packages/d1/cf/d8ae8a80dd7bafab395ea7681c10237311bbf34d37704a8c744e7cf31fc7/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e4ff8e37f99cf1da89e255e07c9c4b37c214038c4283707bdec308cb1b0ea1f", size = 464197, upload-time = "2026-05-18T04:30:09.914Z" }, + { url = "https://files.pythonhosted.org/packages/7c/8a/3076c496ca8dafe0e8cd03fcebdfc47be4b1174b4e5b24ff6e396e6b3af2/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:054dc20fd2e3132b4c3883b4a00d72fd6e1f56fdaf89fccd12e8057d74cd74d7", size = 453181, upload-time = "2026-05-18T04:30:14.829Z" }, + { url = "https://files.pythonhosted.org/packages/e5/10/9745e17c98e7b8a86454df0a3c7b5686bd650383f1e9f26e4ebcbd6cc0c0/watchfiles-1.2.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:e140ed30ebde76796b686e67c182cff10ea2fbab186fafd1560f74bb5a473a6e", size = 465109, upload-time = "2026-05-18T04:30:28.123Z" }, + { url = "https://files.pythonhosted.org/packages/8f/95/8ef4a95481d3e0cb52d62a06fa6e972e81424be2d9698b91a2fecca9904c/watchfiles-1.2.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:bb7e52ecf68ba46d22df23467b87cffeb2146908aa523ebfe803019618cfda06", size = 630653, upload-time = "2026-05-18T04:31:49.304Z" }, + { url = "https://files.pythonhosted.org/packages/fd/e4/3b3bf36b0f829b50c6ebcb8d031583863c59f923d6a6af3d485e470d0fac/watchfiles-1.2.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:23282a321c8baf9b3a3c4afff673f9fe65eb7fdc2338d765ccad9d3d1916a5ba", size = 657838, upload-time = "2026-05-18T04:31:06.497Z" }, + { url = "https://files.pythonhosted.org/packages/21/b1/6cbbb50c1f3002ab568777d44aa21206dfb8807a840990c4037523b51812/watchfiles-1.2.0-cp314-cp314-win32.whl", hash = "sha256:c0db965c5f79aa49fe672d297cf1febc5ad149b658594944f49a54a2b96270a7", size = 275108, upload-time = "2026-05-18T04:30:06.891Z" }, + { url = "https://files.pythonhosted.org/packages/92/45/190ce6db8dcb4536682cf75d3889ff1a27182a58cb519d343cb6d9ea63d8/watchfiles-1.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:71283b39fd17e5408eb123bd37aeecfd9d54c81fc184421943208aadb879d103", size = 288441, upload-time = "2026-05-18T04:32:12.901Z" }, + { url = "https://files.pythonhosted.org/packages/74/0d/3eae1c2313ab08378431d907c3f8095ecca00f3eda33111cf4f0f2591799/watchfiles-1.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:c5c19526f4e54a00f2666a6c0e9e40d582c09e865055ea7378bf0009aab857b3", size = 280684, upload-time = "2026-05-18T04:31:26.902Z" }, + { url = "https://files.pythonhosted.org/packages/b1/75/fb64e6c25d6b5ca636d03df34ffb1c6e9873303e76d27967e045f8df088f/watchfiles-1.2.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:d73a585accffa5ae39c17264c36ec3166d2fad7000c780f5ef83b2722afb9dd2", size = 398857, upload-time = "2026-05-18T04:32:17.108Z" }, + { url = "https://files.pythonhosted.org/packages/73/4e/9f7adf01754cbf81843722ccfec169d8f26c69778281a302855cecd2ee08/watchfiles-1.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ae99b14c5f21e026e0e9d96f40e07d8570ebee6cafd9d8fc318354606daa7a28", size = 392413, upload-time = "2026-05-18T04:31:07.911Z" }, + { url = "https://files.pythonhosted.org/packages/47/c8/bec626bcc2d69f44b9acb24ce7d60ed7b16b73628eea747fcbd169d8edda/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4429f3b105524a10b72c3a819b091c495d2811d419c1e1e8df773a5a5974f831", size = 452409, upload-time = "2026-05-18T04:31:20.142Z" }, + { url = "https://files.pythonhosted.org/packages/00/b7/b6362068e81e7c556d155a34c35d40ac3ef42d747b06d7f6e5bf58e359c2/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:43d818978d06062d9b22c4fab2ebe44cf5213d42dc8e62bda8c2760cfa2eeb33", size = 458827, upload-time = "2026-05-18T04:32:06.219Z" }, + { url = "https://files.pythonhosted.org/packages/67/f8/9a813fa42afb1e0b4625e75f0479826644d3ee8dc287e093799bc01f390c/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b9f732dc58b2dbe69e464ccf8fff7a03b0dd0be439da4c0720d3558527d3d6b4", size = 490104, upload-time = "2026-05-18T04:31:56.034Z" }, + { url = "https://files.pythonhosted.org/packages/2f/bf/27dfb6094ca4c9aad21298b5525b6c53cb36121ee454331d05161e58d130/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8f200104103feb097de4cab8fe4f5dd18a2026934c7dea98c55a2f5fd6d5a33b", size = 571360, upload-time = "2026-05-18T04:31:57.133Z" }, + { url = "https://files.pythonhosted.org/packages/fb/39/44a096d67270ea93df91d33877dbe91fbda3aa4f8ec2edf799d93eda8736/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:63ac26eefbf4af1741247d6fb68b11c49a25b2f7413fbd318a83a12aaa9cf666", size = 464644, upload-time = "2026-05-18T04:30:57.33Z" }, + { url = "https://files.pythonhosted.org/packages/0e/80/c7472203bad6268e3ef1ad260739704847898938ad7ea8b63a5131f46b50/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c4997d4e4a55f0d02b6cde327322daf3a0400e5df6c6b15948994bf72497925", size = 454771, upload-time = "2026-05-18T04:30:48.736Z" }, + { url = "https://files.pythonhosted.org/packages/51/cf/3b10b268b4b7f0fc26e9debb5eef1998b515887840f444cd3ec80c688755/watchfiles-1.2.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:4c887eba18b7945ac73067a8b4a66f21cd46c2539b2bc68588f7be6c7eb6d26b", size = 463494, upload-time = "2026-05-18T04:31:33.826Z" }, + { url = "https://files.pythonhosted.org/packages/3d/3e/a4302545cd589262a0dc7d140e86f7688eba3f9c72776c27f7e23b8864c4/watchfiles-1.2.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:3416ff151bb6b5a8d8d11664974fbef4d9305b9b2957839ab5a270468fd8df30", size = 629383, upload-time = "2026-05-18T04:31:15.596Z" }, + { url = "https://files.pythonhosted.org/packages/db/99/d5649df0a9a410d45b7c882304d0b790903ac9b6e8f2cfd12114e0c6b9f2/watchfiles-1.2.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:0e831a271c035d89789cffc386b6aa1375f39f1cd25eb7ca0997e4970d152fc5", size = 656093, upload-time = "2026-05-18T04:31:58.707Z" }, + { url = "https://files.pythonhosted.org/packages/92/b9/362702539275019a54dd2e94511b31a9b89c5f9e6a21966de7eb692549fc/watchfiles-1.2.0-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:37a6721cdf3f65dbb13aa9503510ccb4451603ac837e44d265d7992a597e1374", size = 400109, upload-time = "2026-05-18T04:31:16.879Z" }, + { url = "https://files.pythonhosted.org/packages/8f/75/71d5ba62db781e5587bded1d944c675374bc4aa37ff33d5018d98e8b6538/watchfiles-1.2.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2b37d10b5a63bd4d87e18472d80fa525bd670586fae62e5dd580452764879b65", size = 392167, upload-time = "2026-05-18T04:31:28.058Z" }, + { url = "https://files.pythonhosted.org/packages/3c/01/c66dd95d0423fe30d31820e2d1d5bda773764131bbb6ac0cb1cf303ac328/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a105bc2283f67e8fbec74253ec2d94925de92ed72c0393f1206bf326b7b7b69", size = 452372, upload-time = "2026-05-18T04:31:00.836Z" }, + { url = "https://files.pythonhosted.org/packages/91/15/2fe99557e72f85627c6a8eed50d889e8d101623e060a22ad75b875cb932d/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5327989a465505f05cfe06f04fa9d0c2fd5432bb243e10e6f012b1bdca3c8579", size = 459596, upload-time = "2026-05-18T04:31:34.96Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/d4acfa0023367428ed48351b3b9b267893037b6cadae55620c61c24bcfd4/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ecb47f183a8025b2aa18b546725c3657e542112ae9c0613a2af79b4fa8d04ad7", size = 490869, upload-time = "2026-05-18T04:31:59.923Z" }, + { url = "https://files.pythonhosted.org/packages/a4/5f/3164cbdce06c9fb95c4f7b9e2f9760b5e2797af43a9ecc317ef42a23a278/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8520a4ab0e37f770afc34459c4f8f7019e153f9124dc101c15538365875d1ab2", size = 571641, upload-time = "2026-05-18T04:32:00.948Z" }, + { url = "https://files.pythonhosted.org/packages/41/e6/85d3731c55e65cd7690f3f803d24c139588aaf863e4bf2148fe7a7fa1a19/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:71cd71740ed2c15211ebb237ced4e39a1cdf6f80566e5fe95428da1626f4fde6", size = 464444, upload-time = "2026-05-18T04:30:34.298Z" }, + { url = "https://files.pythonhosted.org/packages/f4/7d/562641012b8b09872742c3b8adf9629ec479fd78f8d68ae4a0c13da8add6/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f88af53d6ddaf72179ef613ddc905e6f4785f712b49b80b3bef9f3525e6194b4", size = 453593, upload-time = "2026-05-18T04:31:23.464Z" }, + { url = "https://files.pythonhosted.org/packages/56/fe/cb8ef3d6f929d14158fdaaad9925985b7310abc9384dcd4d82dd0016fb59/watchfiles-1.2.0-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:cee9d5efd929efdac5f7e58f72b3376f676b64050a91c5b99a7094c5b2317488", size = 465096, upload-time = "2026-05-18T04:31:30.384Z" }, + { url = "https://files.pythonhosted.org/packages/25/91/80908e835e100527a9267147b08c0eee1fa6ab0ffec15edc04d1d44885f7/watchfiles-1.2.0-cp315-cp315-musllinux_1_1_aarch64.whl", hash = "sha256:b718bf356bbc15e559bd8ef41782b573b8ae0e3f177ab244b440568d7ea02cfb", size = 630638, upload-time = "2026-05-18T04:30:49.89Z" }, + { url = "https://files.pythonhosted.org/packages/46/4b/95ab2f256bb4af3cb2eb23b9317bda984ee6e0f11733a5c004a6c95b06e3/watchfiles-1.2.0-cp315-cp315-musllinux_1_1_x86_64.whl", hash = "sha256:922c0e019fe68b3ae392965a766b02a71ba1168c932cebc3733cd52c5fe5b377", size = 657684, upload-time = "2026-05-18T04:31:32.027Z" }, + { url = "https://files.pythonhosted.org/packages/23/f4/7513ef1e85fc4c6331b59479d6d72661fc391fbe543678052ac72c8b6c19/watchfiles-1.2.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:4674d49eb94706dfe666c069fc0a1b646ffcf920473492e209f6d5f60d3f0cc2", size = 403050, upload-time = "2026-05-18T04:30:36.753Z" }, + { url = "https://files.pythonhosted.org/packages/27/0b/a54103cfd732bb703c7a749222011a0483ef3705948dae3b203158601119/watchfiles-1.2.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:094b9b70103d4e963499bdea001ee3c2697b144cd9ae6218a62c0f89ec9e31db", size = 396629, upload-time = "2026-05-18T04:32:03.268Z" }, + { url = "https://files.pythonhosted.org/packages/5e/2c/73f31a3b893886206c3f54d73e8ad8dee58cdb2f69ad2622e0a8a9e07f4e/watchfiles-1.2.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b0ef001f8c25ad0fa9529f914c1600647ecd0f542d11c19b7894768c67b6acb7", size = 457318, upload-time = "2026-05-18T04:31:01.932Z" }, + { url = "https://files.pythonhosted.org/packages/e9/f9/45d021e4a5cc7b9dd567f7cbb06d3b75f751a690063fb6cc7ec60f4e46b7/watchfiles-1.2.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a88fc94e647bc4eec523f1caa540258eb71d14278b9daf72fa1e2658a98df0f0", size = 457771, upload-time = "2026-05-18T04:30:56.331Z" }, ] [[package]] name = "wcwidth" -version = "0.6.0" +version = "0.8.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/35/a2/8e3becb46433538a38726c948d3399905a4c7cabd0df578ede5dc51f0ec2/wcwidth-0.6.0.tar.gz", hash = "sha256:cdc4e4262d6ef9a1a57e018384cbeb1208d8abbc64176027e2c2455c81313159", size = 159684, upload-time = "2026-02-06T19:19:40.919Z" } +sdist = { url = "https://files.pythonhosted.org/packages/34/74/c6428f875774288bec1396f5bfcbc2d925700a4dad61727fd5f2b12f249d/wcwidth-0.8.2.tar.gz", hash = "sha256:91fbef97204b96a3d4d421609b80340b760cf33e26da123ff243d76b1fda8dda", size = 1466253, upload-time = "2026-06-29T18:11:11.601Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/68/5a/199c59e0a824a3db2b89c5d2dade7ab5f9624dbf6448dc291b46d5ec94d3/wcwidth-0.6.0-py3-none-any.whl", hash = "sha256:1a3a1e510b553315f8e146c54764f4fb6264ffad731b3d78088cdb1478ffbdad", size = 94189, upload-time = "2026-02-06T19:19:39.646Z" }, + { url = "https://files.pythonhosted.org/packages/96/42/3e5985a0a7e57de470b320c6d6a1a67c844f6737a587f3d44dd13d1819e7/wcwidth-0.8.2-py3-none-any.whl", hash = "sha256:d63947694a0539a1d51e01eda7caf800c291020e6cdd7e28ad7b14dd33ad4f85", size = 323166, upload-time = "2026-06-29T18:11:09.888Z" }, ] [[package]] name = "websockets" -version = "16.0" +version = "16.1.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/04/24/4b2031d72e840ce4c1ccb255f693b15c334757fc50023e4db9537080b8c4/websockets-16.0.tar.gz", hash = "sha256:5f6261a5e56e8d5c42a4497b364ea24d94d9563e8fbd44e78ac40879c60179b5", size = 179346, upload-time = "2026-01-10T09:23:47.181Z" } +sdist = { url = "https://files.pythonhosted.org/packages/21/f7/bc3a25c5ec26ce62ce487690becc2f3710bbc7b33338f005ad390db0b986/websockets-16.1.1.tar.gz", hash = "sha256:db234eda965dcce15df96bb9709f587cd87d4d52aaf0e80e2f34ec04c7670c57", size = 182204, upload-time = "2026-07-17T22:51:05.858Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/20/74/221f58decd852f4b59cc3354cccaf87e8ef695fede361d03dc9a7396573b/websockets-16.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:04cdd5d2d1dacbad0a7bf36ccbcd3ccd5a30ee188f2560b7a62a30d14107b31a", size = 177343, upload-time = "2026-01-10T09:22:21.28Z" }, - { url = "https://files.pythonhosted.org/packages/19/0f/22ef6107ee52ab7f0b710d55d36f5a5d3ef19e8a205541a6d7ffa7994e5a/websockets-16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8ff32bb86522a9e5e31439a58addbb0166f0204d64066fb955265c4e214160f0", size = 175021, upload-time = "2026-01-10T09:22:22.696Z" }, - { url = "https://files.pythonhosted.org/packages/10/40/904a4cb30d9b61c0e278899bf36342e9b0208eb3c470324a9ecbaac2a30f/websockets-16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:583b7c42688636f930688d712885cf1531326ee05effd982028212ccc13e5957", size = 175320, upload-time = "2026-01-10T09:22:23.94Z" }, - { url = "https://files.pythonhosted.org/packages/9d/2f/4b3ca7e106bc608744b1cdae041e005e446124bebb037b18799c2d356864/websockets-16.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7d837379b647c0c4c2355c2499723f82f1635fd2c26510e1f587d89bc2199e72", size = 183815, upload-time = "2026-01-10T09:22:25.469Z" }, - { url = "https://files.pythonhosted.org/packages/86/26/d40eaa2a46d4302becec8d15b0fc5e45bdde05191e7628405a19cf491ccd/websockets-16.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df57afc692e517a85e65b72e165356ed1df12386ecb879ad5693be08fac65dde", size = 185054, upload-time = "2026-01-10T09:22:27.101Z" }, - { url = "https://files.pythonhosted.org/packages/b0/ba/6500a0efc94f7373ee8fefa8c271acdfd4dca8bd49a90d4be7ccabfc397e/websockets-16.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:2b9f1e0d69bc60a4a87349d50c09a037a2607918746f07de04df9e43252c77a3", size = 184565, upload-time = "2026-01-10T09:22:28.293Z" }, - { url = "https://files.pythonhosted.org/packages/04/b4/96bf2cee7c8d8102389374a2616200574f5f01128d1082f44102140344cc/websockets-16.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:335c23addf3d5e6a8633f9f8eda77efad001671e80b95c491dd0924587ece0b3", size = 183848, upload-time = "2026-01-10T09:22:30.394Z" }, - { url = "https://files.pythonhosted.org/packages/02/8e/81f40fb00fd125357814e8c3025738fc4ffc3da4b6b4a4472a82ba304b41/websockets-16.0-cp310-cp310-win32.whl", hash = "sha256:37b31c1623c6605e4c00d466c9d633f9b812ea430c11c8a278774a1fde1acfa9", size = 178249, upload-time = "2026-01-10T09:22:32.083Z" }, - { url = "https://files.pythonhosted.org/packages/b4/5f/7e40efe8df57db9b91c88a43690ac66f7b7aa73a11aa6a66b927e44f26fa/websockets-16.0-cp310-cp310-win_amd64.whl", hash = "sha256:8e1dab317b6e77424356e11e99a432b7cb2f3ec8c5ab4dabbcee6add48f72b35", size = 178685, upload-time = "2026-01-10T09:22:33.345Z" }, - { url = "https://files.pythonhosted.org/packages/f2/db/de907251b4ff46ae804ad0409809504153b3f30984daf82a1d84a9875830/websockets-16.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:31a52addea25187bde0797a97d6fc3d2f92b6f72a9370792d65a6e84615ac8a8", size = 177340, upload-time = "2026-01-10T09:22:34.539Z" }, - { url = "https://files.pythonhosted.org/packages/f3/fa/abe89019d8d8815c8781e90d697dec52523fb8ebe308bf11664e8de1877e/websockets-16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:417b28978cdccab24f46400586d128366313e8a96312e4b9362a4af504f3bbad", size = 175022, upload-time = "2026-01-10T09:22:36.332Z" }, - { url = "https://files.pythonhosted.org/packages/58/5d/88ea17ed1ded2079358b40d31d48abe90a73c9e5819dbcde1606e991e2ad/websockets-16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:af80d74d4edfa3cb9ed973a0a5ba2b2a549371f8a741e0800cb07becdd20f23d", size = 175319, upload-time = "2026-01-10T09:22:37.602Z" }, - { url = "https://files.pythonhosted.org/packages/d2/ae/0ee92b33087a33632f37a635e11e1d99d429d3d323329675a6022312aac2/websockets-16.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:08d7af67b64d29823fed316505a89b86705f2b7981c07848fb5e3ea3020c1abe", size = 184631, upload-time = "2026-01-10T09:22:38.789Z" }, - { url = "https://files.pythonhosted.org/packages/c8/c5/27178df583b6c5b31b29f526ba2da5e2f864ecc79c99dae630a85d68c304/websockets-16.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7be95cfb0a4dae143eaed2bcba8ac23f4892d8971311f1b06f3c6b78952ee70b", size = 185870, upload-time = "2026-01-10T09:22:39.893Z" }, - { url = "https://files.pythonhosted.org/packages/87/05/536652aa84ddc1c018dbb7e2c4cbcd0db884580bf8e95aece7593fde526f/websockets-16.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d6297ce39ce5c2e6feb13c1a996a2ded3b6832155fcfc920265c76f24c7cceb5", size = 185361, upload-time = "2026-01-10T09:22:41.016Z" }, - { url = "https://files.pythonhosted.org/packages/6d/e2/d5332c90da12b1e01f06fb1b85c50cfc489783076547415bf9f0a659ec19/websockets-16.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1c1b30e4f497b0b354057f3467f56244c603a79c0d1dafce1d16c283c25f6e64", size = 184615, upload-time = "2026-01-10T09:22:42.442Z" }, - { url = "https://files.pythonhosted.org/packages/77/fb/d3f9576691cae9253b51555f841bc6600bf0a983a461c79500ace5a5b364/websockets-16.0-cp311-cp311-win32.whl", hash = "sha256:5f451484aeb5cafee1ccf789b1b66f535409d038c56966d6101740c1614b86c6", size = 178246, upload-time = "2026-01-10T09:22:43.654Z" }, - { url = "https://files.pythonhosted.org/packages/54/67/eaff76b3dbaf18dcddabc3b8c1dba50b483761cccff67793897945b37408/websockets-16.0-cp311-cp311-win_amd64.whl", hash = "sha256:8d7f0659570eefb578dacde98e24fb60af35350193e4f56e11190787bee77dac", size = 178684, upload-time = "2026-01-10T09:22:44.941Z" }, - { url = "https://files.pythonhosted.org/packages/84/7b/bac442e6b96c9d25092695578dda82403c77936104b5682307bd4deb1ad4/websockets-16.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:71c989cbf3254fbd5e84d3bff31e4da39c43f884e64f2551d14bb3c186230f00", size = 177365, upload-time = "2026-01-10T09:22:46.787Z" }, - { url = "https://files.pythonhosted.org/packages/b0/fe/136ccece61bd690d9c1f715baaeefd953bb2360134de73519d5df19d29ca/websockets-16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8b6e209ffee39ff1b6d0fa7bfef6de950c60dfb91b8fcead17da4ee539121a79", size = 175038, upload-time = "2026-01-10T09:22:47.999Z" }, - { url = "https://files.pythonhosted.org/packages/40/1e/9771421ac2286eaab95b8575b0cb701ae3663abf8b5e1f64f1fd90d0a673/websockets-16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:86890e837d61574c92a97496d590968b23c2ef0aeb8a9bc9421d174cd378ae39", size = 175328, upload-time = "2026-01-10T09:22:49.809Z" }, - { url = "https://files.pythonhosted.org/packages/18/29/71729b4671f21e1eaa5d6573031ab810ad2936c8175f03f97f3ff164c802/websockets-16.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9b5aca38b67492ef518a8ab76851862488a478602229112c4b0d58d63a7a4d5c", size = 184915, upload-time = "2026-01-10T09:22:51.071Z" }, - { url = "https://files.pythonhosted.org/packages/97/bb/21c36b7dbbafc85d2d480cd65df02a1dc93bf76d97147605a8e27ff9409d/websockets-16.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e0334872c0a37b606418ac52f6ab9cfd17317ac26365f7f65e203e2d0d0d359f", size = 186152, upload-time = "2026-01-10T09:22:52.224Z" }, - { url = "https://files.pythonhosted.org/packages/4a/34/9bf8df0c0cf88fa7bfe36678dc7b02970c9a7d5e065a3099292db87b1be2/websockets-16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a0b31e0b424cc6b5a04b8838bbaec1688834b2383256688cf47eb97412531da1", size = 185583, upload-time = "2026-01-10T09:22:53.443Z" }, - { url = "https://files.pythonhosted.org/packages/47/88/4dd516068e1a3d6ab3c7c183288404cd424a9a02d585efbac226cb61ff2d/websockets-16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:485c49116d0af10ac698623c513c1cc01c9446c058a4e61e3bf6c19dff7335a2", size = 184880, upload-time = "2026-01-10T09:22:55.033Z" }, - { url = "https://files.pythonhosted.org/packages/91/d6/7d4553ad4bf1c0421e1ebd4b18de5d9098383b5caa1d937b63df8d04b565/websockets-16.0-cp312-cp312-win32.whl", hash = "sha256:eaded469f5e5b7294e2bdca0ab06becb6756ea86894a47806456089298813c89", size = 178261, upload-time = "2026-01-10T09:22:56.251Z" }, - { url = "https://files.pythonhosted.org/packages/c3/f0/f3a17365441ed1c27f850a80b2bc680a0fa9505d733fe152fdf5e98c1c0b/websockets-16.0-cp312-cp312-win_amd64.whl", hash = "sha256:5569417dc80977fc8c2d43a86f78e0a5a22fee17565d78621b6bb264a115d4ea", size = 178693, upload-time = "2026-01-10T09:22:57.478Z" }, - { url = "https://files.pythonhosted.org/packages/cc/9c/baa8456050d1c1b08dd0ec7346026668cbc6f145ab4e314d707bb845bf0d/websockets-16.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:878b336ac47938b474c8f982ac2f7266a540adc3fa4ad74ae96fea9823a02cc9", size = 177364, upload-time = "2026-01-10T09:22:59.333Z" }, - { url = "https://files.pythonhosted.org/packages/7e/0c/8811fc53e9bcff68fe7de2bcbe75116a8d959ac699a3200f4847a8925210/websockets-16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:52a0fec0e6c8d9a784c2c78276a48a2bdf099e4ccc2a4cad53b27718dbfd0230", size = 175039, upload-time = "2026-01-10T09:23:01.171Z" }, - { url = "https://files.pythonhosted.org/packages/aa/82/39a5f910cb99ec0b59e482971238c845af9220d3ab9fa76dd9162cda9d62/websockets-16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e6578ed5b6981005df1860a56e3617f14a6c307e6a71b4fff8c48fdc50f3ed2c", size = 175323, upload-time = "2026-01-10T09:23:02.341Z" }, - { url = "https://files.pythonhosted.org/packages/bd/28/0a25ee5342eb5d5f297d992a77e56892ecb65e7854c7898fb7d35e9b33bd/websockets-16.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:95724e638f0f9c350bb1c2b0a7ad0e83d9cc0c9259f3ea94e40d7b02a2179ae5", size = 184975, upload-time = "2026-01-10T09:23:03.756Z" }, - { url = "https://files.pythonhosted.org/packages/f9/66/27ea52741752f5107c2e41fda05e8395a682a1e11c4e592a809a90c6a506/websockets-16.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0204dc62a89dc9d50d682412c10b3542d748260d743500a85c13cd1ee4bde82", size = 186203, upload-time = "2026-01-10T09:23:05.01Z" }, - { url = "https://files.pythonhosted.org/packages/37/e5/8e32857371406a757816a2b471939d51c463509be73fa538216ea52b792a/websockets-16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:52ac480f44d32970d66763115edea932f1c5b1312de36df06d6b219f6741eed8", size = 185653, upload-time = "2026-01-10T09:23:06.301Z" }, - { url = "https://files.pythonhosted.org/packages/9b/67/f926bac29882894669368dc73f4da900fcdf47955d0a0185d60103df5737/websockets-16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6e5a82b677f8f6f59e8dfc34ec06ca6b5b48bc4fcda346acd093694cc2c24d8f", size = 184920, upload-time = "2026-01-10T09:23:07.492Z" }, - { url = "https://files.pythonhosted.org/packages/3c/a1/3d6ccdcd125b0a42a311bcd15a7f705d688f73b2a22d8cf1c0875d35d34a/websockets-16.0-cp313-cp313-win32.whl", hash = "sha256:abf050a199613f64c886ea10f38b47770a65154dc37181bfaff70c160f45315a", size = 178255, upload-time = "2026-01-10T09:23:09.245Z" }, - { url = "https://files.pythonhosted.org/packages/6b/ae/90366304d7c2ce80f9b826096a9e9048b4bb760e44d3b873bb272cba696b/websockets-16.0-cp313-cp313-win_amd64.whl", hash = "sha256:3425ac5cf448801335d6fdc7ae1eb22072055417a96cc6b31b3861f455fbc156", size = 178689, upload-time = "2026-01-10T09:23:10.483Z" }, - { url = "https://files.pythonhosted.org/packages/f3/1d/e88022630271f5bd349ed82417136281931e558d628dd52c4d8621b4a0b2/websockets-16.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8cc451a50f2aee53042ac52d2d053d08bf89bcb31ae799cb4487587661c038a0", size = 177406, upload-time = "2026-01-10T09:23:12.178Z" }, - { url = "https://files.pythonhosted.org/packages/f2/78/e63be1bf0724eeb4616efb1ae1c9044f7c3953b7957799abb5915bffd38e/websockets-16.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:daa3b6ff70a9241cf6c7fc9e949d41232d9d7d26fd3522b1ad2b4d62487e9904", size = 175085, upload-time = "2026-01-10T09:23:13.511Z" }, - { url = "https://files.pythonhosted.org/packages/bb/f4/d3c9220d818ee955ae390cf319a7c7a467beceb24f05ee7aaaa2414345ba/websockets-16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fd3cb4adb94a2a6e2b7c0d8d05cb94e6f1c81a0cf9dc2694fb65c7e8d94c42e4", size = 175328, upload-time = "2026-01-10T09:23:14.727Z" }, - { url = "https://files.pythonhosted.org/packages/63/bc/d3e208028de777087e6fb2b122051a6ff7bbcca0d6df9d9c2bf1dd869ae9/websockets-16.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:781caf5e8eee67f663126490c2f96f40906594cb86b408a703630f95550a8c3e", size = 185044, upload-time = "2026-01-10T09:23:15.939Z" }, - { url = "https://files.pythonhosted.org/packages/ad/6e/9a0927ac24bd33a0a9af834d89e0abc7cfd8e13bed17a86407a66773cc0e/websockets-16.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:caab51a72c51973ca21fa8a18bd8165e1a0183f1ac7066a182ff27107b71e1a4", size = 186279, upload-time = "2026-01-10T09:23:17.148Z" }, - { url = "https://files.pythonhosted.org/packages/b9/ca/bf1c68440d7a868180e11be653c85959502efd3a709323230314fda6e0b3/websockets-16.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19c4dc84098e523fd63711e563077d39e90ec6702aff4b5d9e344a60cb3c0cb1", size = 185711, upload-time = "2026-01-10T09:23:18.372Z" }, - { url = "https://files.pythonhosted.org/packages/c4/f8/fdc34643a989561f217bb477cbc47a3a07212cbda91c0e4389c43c296ebf/websockets-16.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a5e18a238a2b2249c9a9235466b90e96ae4795672598a58772dd806edc7ac6d3", size = 184982, upload-time = "2026-01-10T09:23:19.652Z" }, - { url = "https://files.pythonhosted.org/packages/dd/d1/574fa27e233764dbac9c52730d63fcf2823b16f0856b3329fc6268d6ae4f/websockets-16.0-cp314-cp314-win32.whl", hash = "sha256:a069d734c4a043182729edd3e9f247c3b2a4035415a9172fd0f1b71658a320a8", size = 177915, upload-time = "2026-01-10T09:23:21.458Z" }, - { url = "https://files.pythonhosted.org/packages/8a/f1/ae6b937bf3126b5134ce1f482365fde31a357c784ac51852978768b5eff4/websockets-16.0-cp314-cp314-win_amd64.whl", hash = "sha256:c0ee0e63f23914732c6d7e0cce24915c48f3f1512ec1d079ed01fc629dab269d", size = 178381, upload-time = "2026-01-10T09:23:22.715Z" }, - { url = "https://files.pythonhosted.org/packages/06/9b/f791d1db48403e1f0a27577a6beb37afae94254a8c6f08be4a23e4930bc0/websockets-16.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:a35539cacc3febb22b8f4d4a99cc79b104226a756aa7400adc722e83b0d03244", size = 177737, upload-time = "2026-01-10T09:23:24.523Z" }, - { url = "https://files.pythonhosted.org/packages/bd/40/53ad02341fa33b3ce489023f635367a4ac98b73570102ad2cdd770dacc9a/websockets-16.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:b784ca5de850f4ce93ec85d3269d24d4c82f22b7212023c974c401d4980ebc5e", size = 175268, upload-time = "2026-01-10T09:23:25.781Z" }, - { url = "https://files.pythonhosted.org/packages/74/9b/6158d4e459b984f949dcbbb0c5d270154c7618e11c01029b9bbd1bb4c4f9/websockets-16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:569d01a4e7fba956c5ae4fc988f0d4e187900f5497ce46339c996dbf24f17641", size = 175486, upload-time = "2026-01-10T09:23:27.033Z" }, - { url = "https://files.pythonhosted.org/packages/e5/2d/7583b30208b639c8090206f95073646c2c9ffd66f44df967981a64f849ad/websockets-16.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:50f23cdd8343b984957e4077839841146f67a3d31ab0d00e6b824e74c5b2f6e8", size = 185331, upload-time = "2026-01-10T09:23:28.259Z" }, - { url = "https://files.pythonhosted.org/packages/45/b0/cce3784eb519b7b5ad680d14b9673a31ab8dcb7aad8b64d81709d2430aa8/websockets-16.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:152284a83a00c59b759697b7f9e9cddf4e3c7861dd0d964b472b70f78f89e80e", size = 186501, upload-time = "2026-01-10T09:23:29.449Z" }, - { url = "https://files.pythonhosted.org/packages/19/60/b8ebe4c7e89fb5f6cdf080623c9d92789a53636950f7abacfc33fe2b3135/websockets-16.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bc59589ab64b0022385f429b94697348a6a234e8ce22544e3681b2e9331b5944", size = 186062, upload-time = "2026-01-10T09:23:31.368Z" }, - { url = "https://files.pythonhosted.org/packages/88/a8/a080593f89b0138b6cba1b28f8df5673b5506f72879322288b031337c0b8/websockets-16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32da954ffa2814258030e5a57bc73a3635463238e797c7375dc8091327434206", size = 185356, upload-time = "2026-01-10T09:23:32.627Z" }, - { url = "https://files.pythonhosted.org/packages/c2/b6/b9afed2afadddaf5ebb2afa801abf4b0868f42f8539bfe4b071b5266c9fe/websockets-16.0-cp314-cp314t-win32.whl", hash = "sha256:5a4b4cc550cb665dd8a47f868c8d04c8230f857363ad3c9caf7a0c3bf8c61ca6", size = 178085, upload-time = "2026-01-10T09:23:33.816Z" }, - { url = "https://files.pythonhosted.org/packages/9f/3e/28135a24e384493fa804216b79a6a6759a38cc4ff59118787b9fb693df93/websockets-16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b14dc141ed6d2dde437cddb216004bcac6a1df0935d79656387bd41632ba0bbd", size = 178531, upload-time = "2026-01-10T09:23:35.016Z" }, - { url = "https://files.pythonhosted.org/packages/72/07/c98a68571dcf256e74f1f816b8cc5eae6eb2d3d5cfa44d37f801619d9166/websockets-16.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:349f83cd6c9a415428ee1005cadb5c2c56f4389bc06a9af16103c3bc3dcc8b7d", size = 174947, upload-time = "2026-01-10T09:23:36.166Z" }, - { url = "https://files.pythonhosted.org/packages/7e/52/93e166a81e0305b33fe416338be92ae863563fe7bce446b0f687b9df5aea/websockets-16.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:4a1aba3340a8dca8db6eb5a7986157f52eb9e436b74813764241981ca4888f03", size = 175260, upload-time = "2026-01-10T09:23:37.409Z" }, - { url = "https://files.pythonhosted.org/packages/56/0c/2dbf513bafd24889d33de2ff0368190a0e69f37bcfa19009ef819fe4d507/websockets-16.0-pp311-pypy311_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f4a32d1bd841d4bcbffdcb3d2ce50c09c3909fbead375ab28d0181af89fd04da", size = 176071, upload-time = "2026-01-10T09:23:39.158Z" }, - { url = "https://files.pythonhosted.org/packages/a5/8f/aea9c71cc92bf9b6cc0f7f70df8f0b420636b6c96ef4feee1e16f80f75dd/websockets-16.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0298d07ee155e2e9fda5be8a9042200dd2e3bb0b8a38482156576f863a9d457c", size = 176968, upload-time = "2026-01-10T09:23:41.031Z" }, - { url = "https://files.pythonhosted.org/packages/9a/3f/f70e03f40ffc9a30d817eef7da1be72ee4956ba8d7255c399a01b135902a/websockets-16.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:a653aea902e0324b52f1613332ddf50b00c06fdaf7e92624fbf8c77c78fa5767", size = 178735, upload-time = "2026-01-10T09:23:42.259Z" }, - { url = "https://files.pythonhosted.org/packages/6f/28/258ebab549c2bf3e64d2b0217b973467394a9cea8c42f70418ca2c5d0d2e/websockets-16.0-py3-none-any.whl", hash = "sha256:1637db62fad1dc833276dded54215f2c7fa46912301a24bd94d45d46a011ceec", size = 171598, upload-time = "2026-01-10T09:23:45.395Z" }, + { url = "https://files.pythonhosted.org/packages/08/e7/d1671fb984f9dd844e1da5288070c7c23c9eaba3082d3871aae19c3ab8b9/websockets-16.1.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:49ae99bdfcae803a885c926bf14f886196e84925395bb3f568fef5c0f0979d7d", size = 179570, upload-time = "2026-07-17T22:48:24.032Z" }, + { url = "https://files.pythonhosted.org/packages/99/f5/70df723bf571f5e0b1b845e0a4ff1c966eeb84f667599fc251caa37d15a3/websockets-16.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5bfd1ac19b1b9986a9c95a82d5e23a391ebb09e12c34d7be6094b86efcc35731", size = 177252, upload-time = "2026-07-17T22:48:25.775Z" }, + { url = "https://files.pythonhosted.org/packages/90/72/2f14b2e167170b8bf1c8bb7f9b0d78000f470d41a2085a91f33e3917b6c9/websockets-16.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9246a0d063cfcbcc85f2359dd6876d681213f4790832272aa16641b4ed5d64d4", size = 177530, upload-time = "2026-07-17T22:48:27.337Z" }, + { url = "https://files.pythonhosted.org/packages/f3/18/a17e2f0cde02dc10154c808deed7e1d8528afff93612f70d3f0a5b19b011/websockets-16.1.1-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1214e673c404684b9bf7154f5cf43b45025b1a6160fac3a9e438e9c1a97e22cb", size = 186038, upload-time = "2026-07-17T22:48:28.756Z" }, + { url = "https://files.pythonhosted.org/packages/d5/b0/41de283899cf5929d637b72a508cdbc9aa40dc0f317c6b77613fd1000488/websockets-16.1.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90001d893bc368e302ef168d82130b4e4fdd27b85fa094682df9b667c2d48838", size = 187278, upload-time = "2026-07-17T22:48:30.328Z" }, + { url = "https://files.pythonhosted.org/packages/50/61/874aab5257e027f9f61b5004cec65e592babca7942b1bc09f38e72b7f1fd/websockets-16.1.1-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:130937b167a52af203c8d58e78d67705874e82759862e3b9671a452fec4abc87", size = 189936, upload-time = "2026-07-17T22:48:31.896Z" }, + { url = "https://files.pythonhosted.org/packages/a6/1a/42173913ac5519607220849ed417c864d77384e4119f06dbba964a50f096/websockets-16.1.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9c9f23004a3d40e89c01a7955d186a6cc83418d93b749701944ce2de3e95a1f3", size = 187796, upload-time = "2026-07-17T22:48:33.344Z" }, + { url = "https://files.pythonhosted.org/packages/1b/f4/37c1840bd89b529479aec41470b97b7c683b107ca90b6399ac5afb99dedf/websockets-16.1.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f55f0b01956a094c8587146d9558c91937e78789c333860ffaf35931a6e5dbc4", size = 186481, upload-time = "2026-07-17T22:48:34.843Z" }, + { url = "https://files.pythonhosted.org/packages/9e/70/652d9b964adcfbeb056f42e0ca6bece34d108fe75534e74df20643cae199/websockets-16.1.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6aaface73b9c71974c6497366d8b9628357f6c9749e09c4ea3610176c63f2ae3", size = 184351, upload-time = "2026-07-17T22:48:36.307Z" }, + { url = "https://files.pythonhosted.org/packages/13/f1/af3850e5d48d482921985be72ebcb169c6180b3a77b57bd612deebcee23b/websockets-16.1.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:dc0fad4933f427acd5b1cec210f3ea6dce7089e1724e4b9ec6ef47c6c04d1b3b", size = 186791, upload-time = "2026-07-17T22:48:37.762Z" }, + { url = "https://files.pythonhosted.org/packages/1d/40/1a4e3ed4969ec378dcad337e5f1472c5e292cb3e733bc392f0dc2e230abd/websockets-16.1.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:f2769a0344a09e9ccf5b3cce538bc75a51b53eff3275d3896310c8552049195d", size = 185413, upload-time = "2026-07-17T22:48:39.127Z" }, + { url = "https://files.pythonhosted.org/packages/aa/3e/4e3fa1afe8f1a6a780434cd9ba8eb422632b044eff3dd73f6af67523c147/websockets-16.1.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:f70541f3104339f59f830522d94ebadb1bf47426287381623443d8bb1cdbf33d", size = 187178, upload-time = "2026-07-17T22:48:40.676Z" }, + { url = "https://files.pythonhosted.org/packages/71/ab/dd742766aa5dda7f349be0de49e4d565b84cf6f7f7fa02e07692f0f2bdd9/websockets-16.1.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:dc385593a42e31cd6fb60c19f0ecb015b386603818fc2c6c274fb42bd2bb4165", size = 185051, upload-time = "2026-07-17T22:48:42.098Z" }, + { url = "https://files.pythonhosted.org/packages/ae/f5/76438c6560f416f1c0a7f587679fb97cc6e99ed336011d43ce2002dd27c1/websockets-16.1.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:387e8e4aa5df2f90b198fa3cad3478822a89cf905b6a6d6c97dc3664689640cc", size = 185846, upload-time = "2026-07-17T22:48:43.472Z" }, + { url = "https://files.pythonhosted.org/packages/62/12/5c0320f2127823d27b2d56d611d31b0b284ad4edcb41364d66bf4c92b537/websockets-16.1.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:fd46fff7eb62c24804d234f0051c7a8ea81285ad63e0337d3dcf33ca82aee58a", size = 186066, upload-time = "2026-07-17T22:48:44.884Z" }, + { url = "https://files.pythonhosted.org/packages/a2/97/875986b857b955c3f9dd192cb8a1af81254dfb2ea22cc9590f0a1e020b8b/websockets-16.1.1-cp310-cp310-win32.whl", hash = "sha256:7883388947767080f094950b342b30d35a2a06b849cd967c422fa0db72b40ea9", size = 179940, upload-time = "2026-07-17T22:48:46.481Z" }, + { url = "https://files.pythonhosted.org/packages/54/82/1013a5fe7ddae8e102bc3b4b39db81d8d28fd02100a324ce6ede8cd832b1/websockets-16.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:d57685547e0060cc6fd90ee6a28405d6bd395e525545f13c8d7cd99c78afd79f", size = 180239, upload-time = "2026-07-17T22:48:48.043Z" }, + { url = "https://files.pythonhosted.org/packages/2b/03/47debfe28e9d6d354be5d777b67fd44c359b9eb299a5d103500bd7cc3e37/websockets-16.1.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d0fcf657e9f13ff4b177960ab2200237b12994232dfb6df16f1cfe1d4339f93c", size = 179566, upload-time = "2026-07-17T22:48:49.596Z" }, + { url = "https://files.pythonhosted.org/packages/72/93/31efa1ed78c17e5cfc229fd449e3966e1b9cc15753204cd585cc8dd01f4a/websockets-16.1.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b852788aa51764e2d8e4cf5493d559326bcae5e38d16ba25ffa322b034df272a", size = 177250, upload-time = "2026-07-17T22:48:50.942Z" }, + { url = "https://files.pythonhosted.org/packages/01/4a/542378ab3972b0c1cf1df3df3eff9591cea0d30c58c3aa3c4ddbc244e787/websockets-16.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1427fb4cf0d72f66333e2cacc3ff5f575bf2d7008166ce991a4a470b21d51a22", size = 177528, upload-time = "2026-07-17T22:48:52.59Z" }, + { url = "https://files.pythonhosted.org/packages/33/d9/162321f63c7eed558e9e1798ed7a1e34a4f6dab51f35419e4ed7a4907979/websockets-16.1.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:da4ca1a9d72f9030b3146b8d7022719a9f3d478f61efe6f7dd51d243f61c51b2", size = 186859, upload-time = "2026-07-17T22:48:53.915Z" }, + { url = "https://files.pythonhosted.org/packages/de/09/87df740f7430ce564bd52402e9c9458d4d0459cc7d2ee29e530c8204851b/websockets-16.1.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:86d7f0f8bdb25d2c632b72527325e4776430fd5bc61b9118de4e2b8ddb5f5b01", size = 188095, upload-time = "2026-07-17T22:48:55.384Z" }, + { url = "https://files.pythonhosted.org/packages/d2/12/3d2703af7cc095f3c81904c92208cc1ae79affbc67376944b50ee9301f73/websockets-16.1.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7dfcad78ea1492ee3a9ec765cb7f51bbc17d477107aaf6b22abf7b2558d1c5a0", size = 191385, upload-time = "2026-07-17T22:48:56.742Z" }, + { url = "https://files.pythonhosted.org/packages/1d/69/986aa0234a964a00f5149cfc46e136e96c8faad1c783474550f40d31aef4/websockets-16.1.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fb9a0a6dc3d1b3986cb88091b6899f0396651e0f74e2c9766ab8d6ffc3842e29", size = 188653, upload-time = "2026-07-17T22:48:58.134Z" }, + { url = "https://files.pythonhosted.org/packages/35/6b/10f9d03e3970a69ba67bd3b46b87a929b586d0300fadbfe14f57c1f85490/websockets-16.1.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29dfa8114c4a620c69591c5973860f768eac29d3fd6904f37f34266cb219c512", size = 187426, upload-time = "2026-07-17T22:48:59.515Z" }, + { url = "https://files.pythonhosted.org/packages/56/db/bb3aad62bf63d8bb3f0634b2eabffcfb3677a34bd19492110ff6869cf703/websockets-16.1.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6ff9417c0ada4d0f7d212f928303e5579bdf3ace4c802fa4afabb30995da58c3", size = 184882, upload-time = "2026-07-17T22:49:00.916Z" }, + { url = "https://files.pythonhosted.org/packages/6c/4c/c09a2ea9bfbeccce52fdc383e5f28af4bc8843338aabac28c81489af6120/websockets-16.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8fe0b50da2d84535fb4f7b4bfa951280f97ce3d558a0443b541166d609e67b57", size = 187584, upload-time = "2026-07-17T22:49:02.283Z" }, + { url = "https://files.pythonhosted.org/packages/c7/8b/31bb4eb4d9eaacf1fdd39d115772a8aeaedfc19b5dc262e57ffbc8a9d42c/websockets-16.1.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:34420aaa64440ebd51ac72ca8a45ef4626429438c9b02e633ae412ed43f925d3", size = 186174, upload-time = "2026-07-17T22:49:03.973Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e4/dc02d725610a1ad49e193ef91a548194d71bdc6cdf27da83067dd1f73995/websockets-16.1.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:a6a61aff018180c9c50b7b0da33bfd29d378af3497429c95006c589a23a11648", size = 187986, upload-time = "2026-07-17T22:49:05.553Z" }, + { url = "https://files.pythonhosted.org/packages/e0/73/30ed84c8bfd14c73d4af29d5ed9323c3073b48e0b7b23b67070f4e7fd59b/websockets-16.1.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:04fd29a0e2fe9414a95b00e92c67ae51bf900c50c0f8a4b2dafdad621f49ea1d", size = 185565, upload-time = "2026-07-17T22:49:06.959Z" }, + { url = "https://files.pythonhosted.org/packages/7d/d3/4be8d4959f51e31b4f8fc0ece12b45bd3b6c0d15ea23b9990d9c11fc805f/websockets-16.1.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5c31aa7e39ee3e8a358573257f1c0bb5c52430d1b637030dd9c8cc2c282926be", size = 186598, upload-time = "2026-07-17T22:49:08.293Z" }, + { url = "https://files.pythonhosted.org/packages/26/fa/abb38597a52d84ed9cfacadc7a0c6f2db282c0ab23cdf72b58a666a21227/websockets-16.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d14bfb217eb4701e850f1525c9d29d79c44794cdf1c299ead25f39f8c78dea81", size = 186834, upload-time = "2026-07-17T22:49:09.766Z" }, + { url = "https://files.pythonhosted.org/packages/59/80/1119ad08a228b90c4eb77fbe48df7836731a605f5f881ba701ca826a4a65/websockets-16.1.1-cp311-cp311-win32.whl", hash = "sha256:2e28e602bb13da44fbe518c1781a88e3b9d4c3d48d02c9bad83e546164336f57", size = 179940, upload-time = "2026-07-17T22:49:11.196Z" }, + { url = "https://files.pythonhosted.org/packages/71/b2/e511c1c6f64a95c2f3fc54bffda0e14eaa7e9442be605c29270f7589b918/websockets-16.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:7421fad442de870a8cbf2287d1cad7e706ece0dbfeba5e911df132cbdc1cb56a", size = 180239, upload-time = "2026-07-17T22:49:12.519Z" }, + { url = "https://files.pythonhosted.org/packages/17/9d/681cda21c9eee743203a6cb79b9d3d05adad9aa60ec660c6c9bf4dd619ca/websockets-16.1.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:cc97814dfb786a83b6e2dc2e79351e1b83e6d715647d6887fcabd83026417a00", size = 179600, upload-time = "2026-07-17T22:49:13.92Z" }, + { url = "https://files.pythonhosted.org/packages/fb/8d/6195a88b45e8d2a8f745fc2046e36f885a3c9763e6767d2c46229bf9510c/websockets-16.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e047dc87ef7ca50f4d309bf775ad4a71711c58556d75d7bd0604b2317f43e94b", size = 177272, upload-time = "2026-07-17T22:49:15.453Z" }, + { url = "https://files.pythonhosted.org/packages/73/e3/fe2d498c64dea0095c9a9f9a351af4cd6eef31b618395582bc1f38ba45ff/websockets-16.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:01fbdcbac298efe19360b94bc0039c8f746f0220ba570f327577bfee81059175", size = 177542, upload-time = "2026-07-17T22:49:16.875Z" }, + { url = "https://files.pythonhosted.org/packages/fe/ed/f1831681fce0e3242346e5458486003c5f124ed69e5e0b847fd029db4973/websockets-16.1.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0f62863e8a00a6d33c3d6566ec0b89f23787b747ffe0c3bc71ec0e76b82c94b1", size = 187137, upload-time = "2026-07-17T22:49:18.323Z" }, + { url = "https://files.pythonhosted.org/packages/6f/79/4ff9dcc1bb46f6b4c536936dde1fd60f9b564f3304307274db97f4c9496d/websockets-16.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8087e82f842609734c9b5a1330464f8e94e346ba0e18c832c08bafa4b0d63c15", size = 188374, upload-time = "2026-07-17T22:49:19.65Z" }, + { url = "https://files.pythonhosted.org/packages/62/c3/5c49b6efb36cab733d23773f6de575e1dba65736ead17d5d2b2a1daef779/websockets-16.1.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2bb5d041a8307d2e18782e7ce777f6fdb1e8c2f5d09291484b18c294b789d9aa", size = 191155, upload-time = "2026-07-17T22:49:21.331Z" }, + { url = "https://files.pythonhosted.org/packages/6e/f6/56ccceda3a4838d18f1d40821480da4775397e8b1eecf4031e20c50e2e90/websockets-16.1.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1db4de4a0e95673f7545d393c49eeb0c2f18ac1ef93073218c79d5cdb2ee75ab", size = 189011, upload-time = "2026-07-17T22:49:22.889Z" }, + { url = "https://files.pythonhosted.org/packages/86/d6/ad5286241a2bce1107e2798d3bfbd62cf79aee167bdb654f8cb1e9dbf949/websockets-16.1.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f17dbe07eb3ea7f99e4df9b7e0efefe80fbf30d37a8cc4d561a0aed310bc8847", size = 187766, upload-time = "2026-07-17T22:49:24.339Z" }, + { url = "https://files.pythonhosted.org/packages/bc/67/d65c970b7e347fdca69479beb7811c2060529956730a7a4e3ae7c66b0e31/websockets-16.1.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4b57693728576d84ede0a77987ab16881b783d2cd9f1dc180a8fbbc3f79c4428", size = 185173, upload-time = "2026-07-17T22:49:25.743Z" }, + { url = "https://files.pythonhosted.org/packages/1d/5b/14af3cd4ee69d8ea9baca58f3dc3cfb1ba78332a347fd478cb096549d60e/websockets-16.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2a636ff1e7a5c4edf71ef0e79adae7f25dba93b4fcbe3dc958733477ffeb0eaf", size = 187809, upload-time = "2026-07-17T22:49:27.147Z" }, + { url = "https://files.pythonhosted.org/packages/7b/11/be301710d70de97e3e7b3586e6d492c9c06d6a61bf1c2202c36cf0c75607/websockets-16.1.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:d6bec75c290fe484a8ba4cacdf838501e17c06ecfbbf31eede81a9e431bd7751", size = 186412, upload-time = "2026-07-17T22:49:28.611Z" }, + { url = "https://files.pythonhosted.org/packages/db/07/fe1435bf6fe738a3d3b54dbe0c18dabf12cba4d909ac8b58b539ce27c1f4/websockets-16.1.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:54509b8e92fee4453e152b7558ddef37ce9705a044922f2095a6105e3f80c96f", size = 188290, upload-time = "2026-07-17T22:49:29.965Z" }, + { url = "https://files.pythonhosted.org/packages/8a/0a/81f394aff8efcbb01208c1ced77df0a3c7fcce584a88c7273663697946c2/websockets-16.1.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:f0aa4aad3b1b69ad3fd85a0fd0952ec64331c762bd77ec51cc814170873890b2", size = 185844, upload-time = "2026-07-17T22:49:31.447Z" }, + { url = "https://files.pythonhosted.org/packages/39/5c/dd485b995473f415510251fe9bd708f2d24458f439fce958daf8d66dc7c6/websockets-16.1.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:42290eb6db4ccaca7012656738214f8514082fb6fa40cdeb61bb9a471b52e383", size = 186823, upload-time = "2026-07-17T22:49:33.104Z" }, + { url = "https://files.pythonhosted.org/packages/9d/0b/f78de76ff446f1e66af12b43c48a35f31744de93cfdec2f4ea67d5d7bbf1/websockets-16.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:53260c8930da5771cec89439bff99c20c8cb03ddb9588b980697355a83cd4bd3", size = 187102, upload-time = "2026-07-17T22:49:34.616Z" }, + { url = "https://files.pythonhosted.org/packages/37/a1/4cf892007778eaf84ad162bfc98046e0ed89b63ac55949e3236626b2a23f/websockets-16.1.1-cp312-cp312-win32.whl", hash = "sha256:1d27fa8462ad6a1cb36206a3d0640b2333340def181fae11ed7f9adeaa5c0747", size = 179943, upload-time = "2026-07-17T22:49:36.213Z" }, + { url = "https://files.pythonhosted.org/packages/d9/de/6abe251d28c3a3f217096575400b27750b18e0b1d2fff3a2a239960fea07/websockets-16.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:b436f6ec4fc3a6b4237c84d3f83170ed2b40bb584222f0ac47a0c8a5921980c7", size = 180243, upload-time = "2026-07-17T22:49:37.626Z" }, + { url = "https://files.pythonhosted.org/packages/ce/fd/6ec6c6d2850aea25b1b2aa9901a016980bb87d01e89b3eb00470b1b5d471/websockets-16.1.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ab59169ace05dcb49a1d4118f0bde139557adf45091bd85747e36bf5de984dd1", size = 179587, upload-time = "2026-07-17T22:49:38.959Z" }, + { url = "https://files.pythonhosted.org/packages/5f/d8/1d299d2dd34087db39831a34cc645ef8a6f89d78efada6983093513cd81c/websockets-16.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5e3b7d601f6f84156b08cc4a5e541c2b50ad7b36cfc302b657a12477c904a5df", size = 177272, upload-time = "2026-07-17T22:49:40.293Z" }, + { url = "https://files.pythonhosted.org/packages/3d/86/0a70d3ae2f0f2256bb41302d9804dbca65d4360281e7feb3e1f94102ac46/websockets-16.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cd2ca96a082a36964aca83e992f72abeb61b7306c1a6cba4c7d06a7b93750cac", size = 177530, upload-time = "2026-07-17T22:49:41.786Z" }, + { url = "https://files.pythonhosted.org/packages/b5/c2/c676c69444d9db448b3f0a55a98dcc534affce0bce961d9d2f0b8499b10a/websockets-16.1.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f5d497865f05bb222cab7016c6034542e84e5f29f49c6fd3f4939cda7197b5b8", size = 187197, upload-time = "2026-07-17T22:49:43.658Z" }, + { url = "https://files.pythonhosted.org/packages/0b/13/88137fbaf726ebe29d62c1117fa11fa2bbb6209dc79d4ad738efbe36a2aa/websockets-16.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bae954c382e013d5ea5b190d2830526bfa45ad121c326da0049b8c769f185db6", size = 188433, upload-time = "2026-07-17T22:49:45.147Z" }, + { url = "https://files.pythonhosted.org/packages/01/6d/46c2f2ce6751cb26f39293e1ecbf8544cb01321397cd476c2756b98c216d/websockets-16.1.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e09f753a169951eb4f28c2c774f71069304f66e7277e0f5a2892423599cfa854", size = 189868, upload-time = "2026-07-17T22:49:46.581Z" }, + { url = "https://files.pythonhosted.org/packages/29/2b/170a9e8097636cfde4dc3c592b6e00b18a44a2f5407606d96ca542dd5838/websockets-16.1.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:024193f8551a2b0eafbdd160911012c4e6c228c28430c84433253299a9e42d6a", size = 189059, upload-time = "2026-07-17T22:49:47.972Z" }, + { url = "https://files.pythonhosted.org/packages/a7/48/f0d4ebc9ab4b473b8861b9e20fdb663d515d42f7befdf62cdb60fee7a1ec/websockets-16.1.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:aabe464bfd13bd25f4821faf111da6fefdc389f870265a53105580e45b0a2e49", size = 187814, upload-time = "2026-07-17T22:49:49.344Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ba/39a41d3ae8e72696a9492581900611c5a91e2b07563b0bcd2523adea9854/websockets-16.1.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a28fcbc9b6baf54a2e23f8655f308e4ccc6afdd7266f8fe7954f320dcda0f785", size = 185229, upload-time = "2026-07-17T22:49:50.787Z" }, + { url = "https://files.pythonhosted.org/packages/3c/36/ac15b604f850d1907f0a85ed721cefe47cd45034b3620069b829746cccbe/websockets-16.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:79eace538c6a97e96d0d03d4f9d314f9677f5ed85a8a984992ffd90b13cb8a56", size = 187874, upload-time = "2026-07-17T22:49:52.228Z" }, + { url = "https://files.pythonhosted.org/packages/a8/f3/3fbd5d71d59299c3770faa5884d4f45070236ca5a35ab3a61830812c409a/websockets-16.1.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:496af849a472b531f758dbd4d61338f5000538cb1a7b3d20d9d32a264517f509", size = 186469, upload-time = "2026-07-17T22:49:53.776Z" }, + { url = "https://files.pythonhosted.org/packages/b4/fc/dd90349bba58af2a53ef2ddd9c32716c81eb6d59a0687939fff561860878/websockets-16.1.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5283810d2646741a0d8da2aa733d6aefa0545809afccb2a5d105a26bc45125f1", size = 188347, upload-time = "2026-07-17T22:49:55.202Z" }, + { url = "https://files.pythonhosted.org/packages/4c/f3/f73ba86427682da59b78c11d77ba56d5b801c32e84afe79b274bbd6a9bb2/websockets-16.1.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:4e3b680b1e0a27457e727a0d572fd81dffa87b6dbf8b228ab57da64f7d85aead", size = 185903, upload-time = "2026-07-17T22:49:56.75Z" }, + { url = "https://files.pythonhosted.org/packages/34/7c/f95eb20e80104173b3a0a092291f89ea4047ef6e608e0a57ca06eb14eecb/websockets-16.1.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:69159730a823dde3ea8d08783e8d47ef135a6d7e8d44eb127e32b321c9db8e3e", size = 186855, upload-time = "2026-07-17T22:49:58.467Z" }, + { url = "https://files.pythonhosted.org/packages/b0/35/dd875b3e050ff232d60fa377707f890e369f74d134f1be32e8f68879747c/websockets-16.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ed5bb271084b46530ee2ddc0410537a9961152c5ccba2fc98c5276d992ccba87", size = 187140, upload-time = "2026-07-17T22:50:00.016Z" }, + { url = "https://files.pythonhosted.org/packages/e8/dc/5cbfcb41824502f6af93b8f3943a4d06c67c23c7d2e31eb18748c4a5b2a7/websockets-16.1.1-cp313-cp313-win32.whl", hash = "sha256:cfb70b4eb56cac4da0a83588f3ad50d46beb0690391082f3d4e2d488c70b68ea", size = 179928, upload-time = "2026-07-17T22:50:01.685Z" }, + { url = "https://files.pythonhosted.org/packages/b0/c1/71e5deb5b7f8f226997ab64908c184ac3105c0155ce2d486f318e5dd08a8/websockets-16.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:d9531d9cbeac99af6f038fb1bc351403531f7d634a2c2e10e2f7c854c6ed5b68", size = 180242, upload-time = "2026-07-17T22:50:03.117Z" }, + { url = "https://files.pythonhosted.org/packages/73/a2/ba78a164eeea4620df4a4df4bd2ed6017438c4655cc0f36f2c0bc0432355/websockets-16.1.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:443aefe96b7fdb132e2a70806cca1f2af49bb3f28e47abcd7c2e9dcf4d8fa1b8", size = 179635, upload-time = "2026-07-17T22:50:05.001Z" }, + { url = "https://files.pythonhosted.org/packages/b9/08/d26d7a7628cd4ac34cbbdb63ac80914ca842ed8e42938c40a53567806df3/websockets-16.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:6456ff333092d509127d75a638cb411afae8ff17f092635015d1902efec8a293", size = 177320, upload-time = "2026-07-17T22:50:06.427Z" }, + { url = "https://files.pythonhosted.org/packages/0f/45/ebec83e6269536aa5932533c67b0af5c781f3e73fdbcd68672dcf43f4f44/websockets-16.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fce6c48559c86d1ac3632ccb1bebc7d5442fbe79bd9bb0e40379ee54be2a4051", size = 177544, upload-time = "2026-07-17T22:50:07.834Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d5/abc614d2297f6c1c3e01e61260364457a47c25cc1cf6a879038902bc6aa8/websockets-16.1.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:92b820d345f7a3fc7b8163949ee92df910f290c3fc517b3d5301c78065adafe1", size = 187270, upload-time = "2026-07-17T22:50:09.275Z" }, + { url = "https://files.pythonhosted.org/packages/52/71/4c99af3b87dff1b2927981f6876607d4acb45338c665242168d3982f7758/websockets-16.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a606d9c24035242a3e256e9d5b77ed9cd6bccfcb7cf993e5ca3c0f6f68fb6a7", size = 188509, upload-time = "2026-07-17T22:50:10.722Z" }, + { url = "https://files.pythonhosted.org/packages/9b/b4/5c8ca14b0df7eb84ed0524165c5359150210140817a3312aee57bf62a1cf/websockets-16.1.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:414e596c75f74e0994084694189d7dc9229fb278e33064d6784b73ffbba3ca31", size = 189882, upload-time = "2026-07-17T22:50:12.293Z" }, + { url = "https://files.pythonhosted.org/packages/25/c1/bedfba9e70557129cb8083748d167bdcc01483dedf0f0df143676df05cbe/websockets-16.1.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:536676848fc5961aca9d20389951f59169508f765637a172403dc5434d722fa0", size = 189114, upload-time = "2026-07-17T22:50:13.789Z" }, + { url = "https://files.pythonhosted.org/packages/df/09/aa835b2787835aebd839114be5de51b797cb480b63ba42b26d34dfe147cb/websockets-16.1.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:97fd3a0e8b53efa41970ac1dff3d8cf0d2884cadeb4caaf95db7ad1526926ee3", size = 187861, upload-time = "2026-07-17T22:50:15.179Z" }, + { url = "https://files.pythonhosted.org/packages/20/26/f6408330694dbc9830857d9d23bc14ac4f6875127a480cfdda8d5ca21198/websockets-16.1.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7b1b19636af86a3c7995d4d028dbe376f39b4bf31541146f9c123582a6c94562", size = 185286, upload-time = "2026-07-17T22:50:16.741Z" }, + { url = "https://files.pythonhosted.org/packages/17/9a/e0675e70dd8a80762cf35bb18799d3f290a4890ffe6439bc51d222796083/websockets-16.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:41c8e77f17294c0ac18008a7309b99b34ee72247ef10b6dff4c3f8b5ac29896b", size = 187935, upload-time = "2026-07-17T22:50:18.213Z" }, + { url = "https://files.pythonhosted.org/packages/33/c1/3234cfb86afde01b81e9bddcc6e534c440975d60a13991259e833069ab3e/websockets-16.1.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:9f63bcef7f4b02b06b35fc01c93b96c43b5e88e1e8868676caacf493d5a31f3a", size = 186444, upload-time = "2026-07-17T22:50:19.67Z" }, + { url = "https://files.pythonhosted.org/packages/89/87/9c15206e1d778923d8daa9657de07aa62ea815e13448319c98458c37b281/websockets-16.1.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:dab9eb87869da2d6ed3af3f3adf28414baae6ec9d4df355ffc18889132f3436c", size = 188409, upload-time = "2026-07-17T22:50:21.28Z" }, + { url = "https://files.pythonhosted.org/packages/f2/00/cf5de5c67676de2d3eef8b2a518f168f6796595447a5b7161ba0d012915c/websockets-16.1.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:43e3a9fdd7cbf7ba6040c31fae0faf84ca1474fef777c4e37912f1540f854499", size = 185958, upload-time = "2026-07-17T22:50:22.719Z" }, + { url = "https://files.pythonhosted.org/packages/62/c0/731b6ddede2e4136912ec4cff2cffbda35af73546be4762c3d7bd3bd79af/websockets-16.1.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:056ae37939ed7e9974f364f5864e76e49182622d8f9751ac1903c0d09b013985", size = 186911, upload-time = "2026-07-17T22:50:24.108Z" }, + { url = "https://files.pythonhosted.org/packages/8c/7f/39c634472c4469a24a7c09cecddffb08fac6d0e74f73881a94ee8a40a196/websockets-16.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a0eadbbf2c30f01efa58e1f110eb6fa293261f6b0b1aa38f7f48707107690af9", size = 187204, upload-time = "2026-07-17T22:50:25.548Z" }, + { url = "https://files.pythonhosted.org/packages/26/89/9667c256c256dafcc62d21328ce7a40067da857969b68ee9af375b0aaf72/websockets-16.1.1-cp314-cp314-win32.whl", hash = "sha256:195c978b065fa40910582464f99d6b15c8b314c68e0546549a55ed83f4735328", size = 179603, upload-time = "2026-07-17T22:50:27.086Z" }, + { url = "https://files.pythonhosted.org/packages/bd/dd/1c099d6c0fc5deb6b46ccdbb6981fdb4b12c917869cb3952408409dc18db/websockets-16.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:4e8d01cc3bcae7bbf8167f944aeafefed590fae5693552bba9794a9df68371cc", size = 179948, upload-time = "2026-07-17T22:50:28.521Z" }, + { url = "https://files.pythonhosted.org/packages/35/25/9956b2d5e0529d5d23924f21bba1440d4c5c88a562e4f08550871ffa97a7/websockets-16.1.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:0ffd3031ea8bda8d61762e84220186105ba3b748b3c8da2ae4f7816fac03e573", size = 179963, upload-time = "2026-07-17T22:50:29.982Z" }, + { url = "https://files.pythonhosted.org/packages/17/06/55ffc976c488b6aee9ea05761ff7c4e88e7c1fd82818c8ca7b556ad2f90c/websockets-16.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:84a2cef8deffbd9ab8ee0ea546a2a6a7030c28f44e6cdd4547dbfeb489eb8999", size = 177497, upload-time = "2026-07-17T22:50:31.396Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e8/f7dac2e980bacc92bdc26cebae4ae4d50cae5380732c50980598fc0bbae4/websockets-16.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3df13f73af9b3b38ab1195eb299ecb67a4330c911c97ae04043ff74085728abe", size = 177698, upload-time = "2026-07-17T22:50:32.829Z" }, + { url = "https://files.pythonhosted.org/packages/b2/39/26762f734113e22da2b942c3aca85798e0c0405d64c256549540ff31e5a1/websockets-16.1.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:23253dd5bcae3f9aaee0a1d30967a8dbd52e5d3cff93a2e5b84df57b77d4750d", size = 187561, upload-time = "2026-07-17T22:50:34.24Z" }, + { url = "https://files.pythonhosted.org/packages/11/94/c3f330851806b9b02138b774d593478323e73c99238681b4b93efe64e02d/websockets-16.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c1c5705e314449e3308872fe084b8571ce078ee4fc55a98a769bdefe5917392", size = 188732, upload-time = "2026-07-17T22:50:36.088Z" }, + { url = "https://files.pythonhosted.org/packages/d1/f2/eb2c450f052de334ae33cf200ece6e87b0e14d186807074e4eb1cd2cdea2/websockets-16.1.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:69e52d175a0a7d1e13b4b67ad41c560b7d98e8c6f6126eb0bda496c784faf8c7", size = 190872, upload-time = "2026-07-17T22:50:38.008Z" }, + { url = "https://files.pythonhosted.org/packages/70/31/2ac8cecf3a74f7fed9132129fc3d90b3998a1554570c11a69b2a8c20332d/websockets-16.1.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1f79c89b5eb034d1722938a891916582f8f7f503f58ca22518a63c3f2cd18499", size = 189305, upload-time = "2026-07-17T22:50:39.53Z" }, + { url = "https://files.pythonhosted.org/packages/6a/cf/8ab19650d3c0d4562c92e70ab47c257c4aa5c6a713ed87fe63766b31fefc/websockets-16.1.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:39f2a024af5c345ffe8fcf1ee18c049c024c94df393bb09b044a6917c77bde43", size = 188033, upload-time = "2026-07-17T22:50:40.912Z" }, + { url = "https://files.pythonhosted.org/packages/66/d7/a49a38a6127a4acb134fb1912b215d900cc657605cff32445bf519f3acc4/websockets-16.1.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:952303a7318d4cbe1011400839bb2051c9f84fa0a35923267f5daba34b15d458", size = 185748, upload-time = "2026-07-17T22:50:42.559Z" }, + { url = "https://files.pythonhosted.org/packages/95/3e/ad1fa40388c7f2e0bb2c7930d0090b6c5498594bd1cdaec18864df3d9e97/websockets-16.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:249116b4a76063d930a46391ad56e135c286e4562a18309029fc2c73f4ed4c62", size = 188285, upload-time = "2026-07-17T22:50:43.974Z" }, + { url = "https://files.pythonhosted.org/packages/35/b8/d5db28ca264b9104f82196f92dc8843e35fd391f763d42e4ad358f5bc97e/websockets-16.1.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:61922544a0587a13fd3f53e4c0e5e606510c7b0d9d22c8444e5fae22a06b38cb", size = 186777, upload-time = "2026-07-17T22:50:45.474Z" }, + { url = "https://files.pythonhosted.org/packages/42/9c/726cb39d0cc43ae848dce4aa2acb04eecc6738b1264ec6d700bf6bcfb9f8/websockets-16.1.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:46dcaa042cd1de6c59e7d9269fa63ff7572b6df40510600b678f0826b3c7af51", size = 188682, upload-time = "2026-07-17T22:50:46.973Z" }, + { url = "https://files.pythonhosted.org/packages/be/c7/1168704de8c2dd483edabe4a22cbe4465dd8be8dd95561d214f9fe092871/websockets-16.1.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:38565aca3e01ea8734e578fb2118dade0ecb0250533f29e22b8d1a7a196cf4d0", size = 186377, upload-time = "2026-07-17T22:50:48.413Z" }, + { url = "https://files.pythonhosted.org/packages/ca/40/f9ff2d630ffce4e7dfea0b2288e1caf9ebbf9ff8a9ec9396136ce8b94935/websockets-16.1.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:42f599f4d48c7e1a3338fdaac3acd075be3b3cf02d4b274f3bf2767aedd3d217", size = 187148, upload-time = "2026-07-17T22:50:49.845Z" }, + { url = "https://files.pythonhosted.org/packages/b5/71/e177c8299f78d7cbe2d14df228643c10c70c0e86e108e092056bbcc16e46/websockets-16.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dcc04fedf83effaeb9cce98abc9469bb1b42ef85f03e01c8c1f4438ef7555737", size = 187578, upload-time = "2026-07-17T22:50:51.619Z" }, + { url = "https://files.pythonhosted.org/packages/49/b2/b6987faf330f5af5c787a2610124c2e8403d51724f9001ec4fff6311fe7a/websockets-16.1.1-cp314-cp314t-win32.whl", hash = "sha256:8483c2096363120eea8b07c06ae7304d520f686665fffd4811fad423930a65d7", size = 179729, upload-time = "2026-07-17T22:50:53.269Z" }, + { url = "https://files.pythonhosted.org/packages/a2/6e/fbac6ed878dd362fbad7d415fa4f84d38e3e33fed8cde45c64e783acf826/websockets-16.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:bcce07e23e5769375158f5efdcdafa8d5cd014b93c6683865b840ed65b96f231", size = 180072, upload-time = "2026-07-17T22:50:54.969Z" }, + { url = "https://files.pythonhosted.org/packages/e1/ed/71fea6e141590cafc40b14dc5943b0845606bee87bdb52a21b6a73eb4311/websockets-16.1.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:820fb8450edddae3812fd58cbc08e2bf22812cb248ecb5f06dbb82119a56e869", size = 177185, upload-time = "2026-07-17T22:50:56.665Z" }, + { url = "https://files.pythonhosted.org/packages/01/ec/00e7eeca200facf9266a83e4cbbf1bed0e67fba1d4d45031d3e5b3d81b5c/websockets-16.1.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:125f22dbefaf1554fea66fc83851490edb284ce4f501d37ffed2752f418332d9", size = 177459, upload-time = "2026-07-17T22:50:58.197Z" }, + { url = "https://files.pythonhosted.org/packages/75/fd/5774c4b33f7c0d8f0c51809c8b3a93456c48e3543579262cfa64eb5f522e/websockets-16.1.1-pp311-pypy311_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:30bbe120437b5648a77d3519b7024ea09530e0b5b18d3698c5a0ae536fe0cc2e", size = 178294, upload-time = "2026-07-17T22:50:59.641Z" }, + { url = "https://files.pythonhosted.org/packages/37/c3/48e2c03d2bd79bb45948841c592d24156312dd5f58cdf8f549febe652fb6/websockets-16.1.1-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b6b9dadbef0cccd9f4c4ee96b08898afa73e26803bbe0f6aeb5bb12b0074206d", size = 179190, upload-time = "2026-07-17T22:51:01.129Z" }, + { url = "https://files.pythonhosted.org/packages/2d/3f/73e511ecf2496ceac57dd4ed8388efe2bcf0769338a2dbf242c8366ae87e/websockets-16.1.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56cd5fc4f10a9ea8aa0804bddb7b42506cf9e136046f3b4c27de8fec9e2ecba5", size = 180330, upload-time = "2026-07-17T22:51:02.603Z" }, + { url = "https://files.pythonhosted.org/packages/be/4d/2d0d67834092e354d2b0498f014a41249a89556bc406cf86f3e1557bb463/websockets-16.1.1-py3-none-any.whl", hash = "sha256:6abbd3e82c731c8e531714466acd5d87b5e88ac3243465337ba71d68e23ae7e3", size = 173814, upload-time = "2026-07-17T22:51:04.184Z" }, ] [[package]] name = "zipp" -version = "3.23.0" +version = "4.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload-time = "2025-06-08T17:06:39.4Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b9/d8/eab98a517c14134c0b2eb4e2387bc5f457334293ec5d2dd3857ec2966802/zipp-4.1.0.tar.gz", hash = "sha256:4cb57381f544315db7688e976e922a2b18cdb513d21cc194eb42232ba2a3e602", size = 26214, upload-time = "2026-05-18T20:08:57.967Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" }, + { url = "https://files.pythonhosted.org/packages/3a/13/547360d81e6d88d58492968ffda9f9542854f11310ee556fef14260cc886/zipp-4.1.0-py3-none-any.whl", hash = "sha256:25ad4e16390cd314347dd8f1de67a2ac538ae658ed4ab9db16029c07c188e97f", size = 10238, upload-time = "2026-05-18T20:08:57.045Z" }, ] From 5a98ceb5caf0879dc75dc732f86e61db58042c20 Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Tue, 28 Jul 2026 17:15:48 -0400 Subject: [PATCH 15/53] Add v4.0.0b1 changelog and updates entries (#4681) * Add v4.0.0b1 changelog and updates entries * Drop meta note from b1 intro; add #4682 under enhancements * File #4682 under fixes * Correct the camelCase rename claim: Python model fields, not the wire * Baseline the b1 changelog on v3.4.5 * Simplify the beta banner --- docs/changelog.mdx | 176 ++++++++++++++++++ docs/docs.json | 2 +- .../upgrading/from-fastmcp-3.mdx | 2 +- docs/updates.mdx | 22 +++ 4 files changed, 200 insertions(+), 2 deletions(-) diff --git a/docs/changelog.mdx b/docs/changelog.mdx index c22d48e74..09a52bbd8 100644 --- a/docs/changelog.mdx +++ b/docs/changelog.mdx @@ -5,6 +5,182 @@ rss: true tag: NEW --- + + +**[v4.0.0b1: Fourgone Conclusion](https://github.com/PrefectHQ/fastmcp/releases/tag/v4.0.0b1)** + +FastMCP 4 rebuilds the framework on the MCP Python SDK v2, and this beta is the first release to run on the SDK's stable 2.0. The SDK v2 rewrote the protocol layer end to end — protocol types moved into a standalone `mcp_types` package, every model field renamed from camelCase to snake_case in Python, and the server's request-handling model replaced — and FastMCP absorbs nearly all of it, so most FastMCP 3 servers run untouched. On that foundation v4 serves the sessionless `2026-07-28` protocol and the older handshake from one server, adds stateless session state and background tasks, makes protocol extensions a first-class surface, and removes server-initiated sampling and roots from the server API. + +### New Features 🎉 +* Migrate to MCP Python SDK v2 by [@jlowin](https://github.com/jlowin) in [#4437](https://github.com/PrefectHQ/fastmcp/pull/4437) +* Teach fastmcp.Client the modern protocol: mode negotiation, MRTR driver, response cache by [@jlowin](https://github.com/jlowin) in [#4450](https://github.com/PrefectHQ/fastmcp/pull/4450) +* Forward-port Hugging Face auth provider by [@jlowin](https://github.com/jlowin) in [#4475](https://github.com/PrefectHQ/fastmcp/pull/4475) +* Add server-side identity assertion (SEP-990 ID-JAG) by [@jlowin](https://github.com/jlowin) in [#4483](https://github.com/PrefectHQ/fastmcp/pull/4483) +* Add guard-mode multi-round-trip tools (SEP-2322) by [@jlowin](https://github.com/jlowin) in [#4544](https://github.com/PrefectHQ/fastmcp/pull/4544) +* Add FastMCP-native server extension API (SEP-2133) by [@jlowin](https://github.com/jlowin) in [#4602](https://github.com/PrefectHQ/fastmcp/pull/4602) +* Add stateless session state (UserSession / SessionId) by [@jlowin](https://github.com/jlowin) in [#4604](https://github.com/PrefectHQ/fastmcp/pull/4604) +* Add background tasks via the io.modelcontextprotocol/tasks extension (SEP-2663) by [@jlowin](https://github.com/jlowin) in [#4603](https://github.com/PrefectHQ/fastmcp/pull/4603) +### Breaking Changes ⚠️ +* Emit one SERVER span per request and adopt spec-correct error codes by [@jlowin](https://github.com/jlowin) in [#4445](https://github.com/PrefectHQ/fastmcp/pull/4445) +* Remove 3.x deprecated module shims and dead parameters by [@jlowin](https://github.com/jlowin) in [#4447](https://github.com/PrefectHQ/fastmcp/pull/4447) +* Remove 3.0-deprecated FastMCP server methods by [@jlowin](https://github.com/jlowin) in [#4451](https://github.com/PrefectHQ/fastmcp/pull/4451) +* Remove 3.x deprecated parameters and object-mode decorators by [@jlowin](https://github.com/jlowin) in [#4453](https://github.com/PrefectHQ/fastmcp/pull/4453) +* Migrate to MCP SDK v2.0.0b2 (httpx2) by [@jlowin](https://github.com/jlowin) in [#4503](https://github.com/PrefectHQ/fastmcp/pull/4503) +* Fix typos by [@szepeviktor](https://github.com/szepeviktor) in [#4498](https://github.com/PrefectHQ/fastmcp/pull/4498) +* Stop proxies from validating backend results or mutating shared transports by [@jlowin](https://github.com/jlowin) in [#4552](https://github.com/PrefectHQ/fastmcp/pull/4552) +* Surface resource, prompt, and proxy errors on the modern protocol by [@jlowin](https://github.com/jlowin) in [#4579](https://github.com/PrefectHQ/fastmcp/pull/4579) +* Negotiate the best mutual protocol era by default by [@jlowin](https://github.com/jlowin) in [#4572](https://github.com/PrefectHQ/fastmcp/pull/4572) +* Remove server-initiated sampling and roots from the server API by [@jlowin](https://github.com/jlowin) in [#4648](https://github.com/PrefectHQ/fastmcp/pull/4648) +* Remove 3.x-era compatibility shims by [@jlowin](https://github.com/jlowin) in [#4661](https://github.com/PrefectHQ/fastmcp/pull/4661) +### Enhancements ✨ +* Deprecate ctx.sample and add clear errors for push features on 2026 connections by [@jlowin](https://github.com/jlowin) in [#4448](https://github.com/PrefectHQ/fastmcp/pull/4448) +* Add server-level cache hints (SEP-2549) by [@jlowin](https://github.com/jlowin) in [#4464](https://github.com/PrefectHQ/fastmcp/pull/4464) +* Add KeyValueResponseCacheStore for distributed client response caching by [@jlowin](https://github.com/jlowin) in [#4479](https://github.com/PrefectHQ/fastmcp/pull/4479) +* Test lifespan fires once per process over HTTP by [@jlowin](https://github.com/jlowin) in [#4480](https://github.com/PrefectHQ/fastmcp/pull/4480) +* Add telemetry off-switch and mcp.protocol.version span attribute by [@jlowin](https://github.com/jlowin) in [#4481](https://github.com/PrefectHQ/fastmcp/pull/4481) +* Trace client task management requests by [@jlowin](https://github.com/jlowin) in [#4525](https://github.com/PrefectHQ/fastmcp/pull/4525) +* Stabilize upgraded ty checks by [@jlowin](https://github.com/jlowin) in [#4526](https://github.com/PrefectHQ/fastmcp/pull/4526) +* Improve DescopeProvider scope discovery and well-known URL support by [@gaokevin1](https://github.com/gaokevin1) in [#4489](https://github.com/PrefectHQ/fastmcp/pull/4489) +* Add examples/ to the ty static-analysis gate by [@jlowin](https://github.com/jlowin) in [#4466](https://github.com/PrefectHQ/fastmcp/pull/4466) +* Expose telemetry attributes on span start by [@zzstoatzz](https://github.com/zzstoatzz) in [#4487](https://github.com/PrefectHQ/fastmcp/pull/4487) +* Fix-issue-4284 : Add Auth0MCPProvider for Auth0 Auth for MCP by [@vijaydeepsinha](https://github.com/vijaydeepsinha) in [#4411](https://github.com/PrefectHQ/fastmcp/pull/4411) +* Run FastMCP middleware for every inbound message by [@jlowin](https://github.com/jlowin) in [#4553](https://github.com/PrefectHQ/fastmcp/pull/4553) +* Add 'prs welcome' label to waive the PR assignment gate by [@jlowin](https://github.com/jlowin) in [#4557](https://github.com/PrefectHQ/fastmcp/pull/4557) +* Rename martian workflows to marvin by [@jlowin](https://github.com/jlowin) in [#4558](https://github.com/PrefectHQ/fastmcp/pull/4558) +* Bump pinned Claude models to current versions by [@jlowin](https://github.com/jlowin) in [#4561](https://github.com/PrefectHQ/fastmcp/pull/4561) +* Make the unit suite fast: in-process HTTP tests, no real sleeps, parallel Windows CI by [@jlowin](https://github.com/jlowin) in [#4554](https://github.com/PrefectHQ/fastmcp/pull/4554) +* Mirror the frontend's protocol era on a proxy's backend connection by [@jlowin](https://github.com/jlowin) in [#4573](https://github.com/PrefectHQ/fastmcp/pull/4573) +* Drop forked client protocol helpers in favor of the SDK's by [@jlowin](https://github.com/jlowin) in [#4574](https://github.com/PrefectHQ/fastmcp/pull/4574) +* Bring the v4 developer notes up to date with what shipped by [@jlowin](https://github.com/jlowin) in [#4581](https://github.com/PrefectHQ/fastmcp/pull/4581) +* Trim fastmcp.types to FastMCP-unique types by [@jlowin](https://github.com/jlowin) in [#4584](https://github.com/PrefectHQ/fastmcp/pull/4584) +* Let a server answer argument-completion requests by [@jlowin](https://github.com/jlowin) in [#4582](https://github.com/PrefectHQ/fastmcp/pull/4582) +* Add machine-to-machine client authentication by [@jlowin](https://github.com/jlowin) in [#4583](https://github.com/PrefectHQ/fastmcp/pull/4583) +* Expose era-neutral client server metadata by [@zzstoatzz](https://github.com/zzstoatzz) in [#4599](https://github.com/PrefectHQ/fastmcp/pull/4599) +* Support routable transport headers for gateways (SEP-2243) by [@jlowin](https://github.com/jlowin) in [#4622](https://github.com/PrefectHQ/fastmcp/pull/4622) +* Emit scope step-up challenges for incremental authorization (SEP-2350) by [@jlowin](https://github.com/jlowin) in [#4623](https://github.com/PrefectHQ/fastmcp/pull/4623) +* Honor OAuth application_type in DCR (SEP-837) by [@jlowin](https://github.com/jlowin) in [#4621](https://github.com/PrefectHQ/fastmcp/pull/4621) +* Drop stale label-noting instructions from CLAUDE.md by [@jlowin](https://github.com/jlowin) in [#4654](https://github.com/PrefectHQ/fastmcp/pull/4654) +* Add require_roles auth check by [@jlowin](https://github.com/jlowin) in [#4656](https://github.com/PrefectHQ/fastmcp/pull/4656) +* Add `valid_scopes` parameter to OIDC proxy valid scopes by [@Educg550](https://github.com/Educg550) in [#4660](https://github.com/PrefectHQ/fastmcp/pull/4660) +* feat: Add telemetry interop mode for FastMCP by [@strawgate](https://github.com/strawgate) in [#4046](https://github.com/PrefectHQ/fastmcp/pull/4046) +* Note that review comment threads should get an acknowledgement by [@jlowin](https://github.com/jlowin) in [#4678](https://github.com/PrefectHQ/fastmcp/pull/4678) +* Soften the review-comment reply guidance by [@jlowin](https://github.com/jlowin) in [#4683](https://github.com/PrefectHQ/fastmcp/pull/4683) +* Resolve review threads on fix, reply on decline by [@jlowin](https://github.com/jlowin) in [#4685](https://github.com/PrefectHQ/fastmcp/pull/4685) +* Move to the stable MCP Python SDK 2.0.0 by [@jlowin](https://github.com/jlowin) in [#4655](https://github.com/PrefectHQ/fastmcp/pull/4655) +### Security 🔒 +* Drive the FastMCP lifespan through the SDK session manager by [@jlowin](https://github.com/jlowin) in [#4446](https://github.com/PrefectHQ/fastmcp/pull/4446) +* Route skill file access through SDK path-security primitives by [@jlowin](https://github.com/jlowin) in [#4449](https://github.com/PrefectHQ/fastmcp/pull/4449) +* Screen templated resource parameters for path traversal by default by [@jlowin](https://github.com/jlowin) in [#4482](https://github.com/PrefectHQ/fastmcp/pull/4482) +* [codex] Add OAuthProxy RFC 9207 issuer responses by [@jlowin](https://github.com/jlowin) in [#4438](https://github.com/PrefectHQ/fastmcp/pull/4438) +* Apply app visibility where no host can by [@jlowin](https://github.com/jlowin) in [#4692](https://github.com/PrefectHQ/fastmcp/pull/4692) +### Fixes 🐞 +* Capture SharedContext for task-enabled Docket servers by [@jlowin](https://github.com/jlowin) in [#4443](https://github.com/PrefectHQ/fastmcp/pull/4443) +* Fix stale mcp.types imports in examples by [@jlowin](https://github.com/jlowin) in [#4452](https://github.com/PrefectHQ/fastmcp/pull/4452) +* Forward-port HTTP host guard compatibility by [@jlowin](https://github.com/jlowin) in [#4474](https://github.com/PrefectHQ/fastmcp/pull/4474) +* Fix Azure scope fallback by [@zzstoatzz](https://github.com/zzstoatzz) in [#4469](https://github.com/PrefectHQ/fastmcp/pull/4469) +* fix(server): omit ScalarElicitationType wrapper title from elicitation schemas by [@syf2211](https://github.com/syf2211) in [#4502](https://github.com/PrefectHQ/fastmcp/pull/4502) +* Skip unsupported JWKS keys instead of failing the whole key set (#4515) by [@earfman](https://github.com/earfman) in [#4517](https://github.com/PrefectHQ/fastmcp/pull/4517) +* Don't mutate the caller's schema in compress_schema by [@winklemad](https://github.com/winklemad) in [#4492](https://github.com/PrefectHQ/fastmcp/pull/4492) +* Forward upstream instructions through create_proxy by [@verdie-g](https://github.com/verdie-g) in [#4512](https://github.com/PrefectHQ/fastmcp/pull/4512) +* Serialize deep object query parameters by [@jlowin](https://github.com/jlowin) in [#4523](https://github.com/PrefectHQ/fastmcp/pull/4523) +* Reject positional-only tool parameters by [@jlowin](https://github.com/jlowin) in [#4524](https://github.com/PrefectHQ/fastmcp/pull/4524) +* Clarify PR-reopen flow and fix label-race that broke auto-reopen by [@jlowin](https://github.com/jlowin) in [#4518](https://github.com/PrefectHQ/fastmcp/pull/4518) +* Clean up disconnected task sessions by [@jlowin](https://github.com/jlowin) in [#4519](https://github.com/PrefectHQ/fastmcp/pull/4519) +* Handle expired OAuth client registrations by [@jlowin](https://github.com/jlowin) in [#4520](https://github.com/PrefectHQ/fastmcp/pull/4520) +* Fix OAuth request annotation after httpx2 migration by [@jlowin](https://github.com/jlowin) in [#4534](https://github.com/PrefectHQ/fastmcp/pull/4534) +* Fix docs banner contrast by [@jlowin](https://github.com/jlowin) in [#4522](https://github.com/PrefectHQ/fastmcp/pull/4522) +* Preserve component metadata in response cache by [@jlowin](https://github.com/jlowin) in [#4521](https://github.com/PrefectHQ/fastmcp/pull/4521) +* Clean up task sessions on connection exit by [@jlowin](https://github.com/jlowin) in [#4535](https://github.com/PrefectHQ/fastmcp/pull/4535) +* Include scopes in auth challenges by [@jlowin](https://github.com/jlowin) in [#4527](https://github.com/PrefectHQ/fastmcp/pull/4527) +* Make examples/ actually trigger the ty gate by [@jlowin](https://github.com/jlowin) in [#4541](https://github.com/PrefectHQ/fastmcp/pull/4541) +* Add subject field to AccessToken initialization by [@piaudonn](https://github.com/piaudonn) in [#4267](https://github.com/PrefectHQ/fastmcp/pull/4267) +* Restore Mintlify's fixed banner positioning by [@jlowin](https://github.com/jlowin) in [#4542](https://github.com/PrefectHQ/fastmcp/pull/4542) +* Fix #4292: SSRF guard breaks OAuth/JWKS fetches behind a corporate HTTP proxy by [@endofcake](https://github.com/endofcake) in [#4412](https://github.com/PrefectHQ/fastmcp/pull/4412) +* Preserve telemetry attributes when a sampler does not forward them by [@jlowin](https://github.com/jlowin) in [#4539](https://github.com/PrefectHQ/fastmcp/pull/4539) +* Speed up the unit test suite, and fix the task-notification race it surfaced by [@jlowin](https://github.com/jlowin) in [#4550](https://github.com/PrefectHQ/fastmcp/pull/4550) +* Fix label triage applying no labels, and make blocked tool calls fail by [@jlowin](https://github.com/jlowin) in [#4555](https://github.com/PrefectHQ/fastmcp/pull/4555) +* Fix AI workflow allowlists being destroyed by tokenization by [@jlowin](https://github.com/jlowin) in [#4560](https://github.com/PrefectHQ/fastmcp/pull/4560) +* Make transformed tool `required` order deterministic by [@Kludex](https://github.com/Kludex) in [#4564](https://github.com/PrefectHQ/fastmcp/pull/4564) +* Stop gather() from creating coroutines it may never schedule by [@jlowin](https://github.com/jlowin) in [#4559](https://github.com/PrefectHQ/fastmcp/pull/4559) +* Restore upgraded dependency checks by [@zzstoatzz](https://github.com/zzstoatzz) in [#4576](https://github.com/PrefectHQ/fastmcp/pull/4576) +* Fix skill frontmatter parsing with UTF-8 BOM by [@hxaxd](https://github.com/hxaxd) in [#4533](https://github.com/PrefectHQ/fastmcp/pull/4533) +* Fix File helper extension handling by [@VectorPeak](https://github.com/VectorPeak) in [#4531](https://github.com/PrefectHQ/fastmcp/pull/4531) +* Fix percent-encoded skill file names unreadable in resources mode by [@jlowin](https://github.com/jlowin) in [#4590](https://github.com/PrefectHQ/fastmcp/pull/4590) +* Fix flaky stdio crash-recovery tests by [@jlowin](https://github.com/jlowin) in [#4594](https://github.com/PrefectHQ/fastmcp/pull/4594) +* Bridge camelCase ToolAnnotations reads by [@zzstoatzz](https://github.com/zzstoatzz) in [#4597](https://github.com/PrefectHQ/fastmcp/pull/4597) +* Preserve raw CallToolResult tool returns by [@LarryHu0217](https://github.com/LarryHu0217) in [#4587](https://github.com/PrefectHQ/fastmcp/pull/4587) +* Advertise only supported token endpoint auth methods in OAuthProxy metadata by [@jlowin](https://github.com/jlowin) in [#4608](https://github.com/PrefectHQ/fastmcp/pull/4608) +* Fix OAuth proxy override typing by [@zzstoatzz](https://github.com/zzstoatzz) in [#4612](https://github.com/PrefectHQ/fastmcp/pull/4612) +* Pin burner-redis below the Windows-crashing 0.1.7 release by [@jlowin](https://github.com/jlowin) in [#4618](https://github.com/PrefectHQ/fastmcp/pull/4618) +* fix : canonical mime type mapping from formats to remove inconsistency #4627 by [@Aman071106](https://github.com/Aman071106) in [#4628](https://github.com/PrefectHQ/fastmcp/pull/4628) +* fix: accept callable roots handlers by [@ShuyingZhang](https://github.com/ShuyingZhang) in [#4639](https://github.com/PrefectHQ/fastmcp/pull/4639) +* Pass the MCP conformance suite's draft and pending scenarios by [@jlowin](https://github.com/jlowin) in [#4650](https://github.com/PrefectHQ/fastmcp/pull/4650) +* Use issuer_url for OAuth issuer identity by [@jlowin](https://github.com/jlowin) in [#4652](https://github.com/PrefectHQ/fastmcp/pull/4652) +* Fix the ty failure blocking upgrade checks on main by [@jlowin](https://github.com/jlowin) in [#4657](https://github.com/PrefectHQ/fastmcp/pull/4657) +* Bind CIMD assertion audience to the advertised token endpoint by [@jlowin](https://github.com/jlowin) in [#4659](https://github.com/PrefectHQ/fastmcp/pull/4659) +* Record effective scopes on the OAuth transaction by [@jlowin](https://github.com/jlowin) in [#4670](https://github.com/PrefectHQ/fastmcp/pull/4670) +* Copy schemas iteratively so deep nesting still compresses by [@jlowin](https://github.com/jlowin) in [#4671](https://github.com/PrefectHQ/fastmcp/pull/4671) +* Fix OpenAPI allOf reference fields by [@hxaxd](https://github.com/hxaxd) in [#4653](https://github.com/PrefectHQ/fastmcp/pull/4653) +* Flatten OpenAPI discriminator subtypes into request bodies by [@jlowin](https://github.com/jlowin) in [#4677](https://github.com/PrefectHQ/fastmcp/pull/4677) +* Let maintenance releases publish without fastmcp-tasks by [@jlowin](https://github.com/jlowin) in [#4676](https://github.com/PrefectHQ/fastmcp/pull/4676) +* Read CLI-scanned MCP config files as UTF-8 explicitly by [@jlowin](https://github.com/jlowin) in [#4690](https://github.com/PrefectHQ/fastmcp/pull/4690) +* Late-bind app tool names so UIs survive composition by [@jlowin](https://github.com/jlowin) in [#4682](https://github.com/PrefectHQ/fastmcp/pull/4682) +### Docs 📚 +* Docs: forward-port v3.4.4 changelog entries by [@jlowin](https://github.com/jlowin) in [#4476](https://github.com/PrefectHQ/fastmcp/pull/4476) +* Document icon theme support by [@jlowin](https://github.com/jlowin) in [#4537](https://github.com/PrefectHQ/fastmcp/pull/4537) +* Add missing 4.0.0 version badge to Path Security docs by [@jlowin](https://github.com/jlowin) in [#4540](https://github.com/PrefectHQ/fastmcp/pull/4540) +* Align server component docs by [@strawgate](https://github.com/strawgate) in [#4260](https://github.com/PrefectHQ/fastmcp/pull/4260) +* Align CLI, deployment, and config docs by [@strawgate](https://github.com/strawgate) in [#4259](https://github.com/PrefectHQ/fastmcp/pull/4259) +* Align client, Apps, and integration docs by [@strawgate](https://github.com/strawgate) in [#4261](https://github.com/PrefectHQ/fastmcp/pull/4261) +* Fix stale MRTR/elicitation framing in client and upgrade docs by [@jlowin](https://github.com/jlowin) in [#4551](https://github.com/PrefectHQ/fastmcp/pull/4551) +* docs: quote pip extras install examples by [@RachGranville](https://github.com/RachGranville) in [#4568](https://github.com/PrefectHQ/fastmcp/pull/4568) +* Document Windows CI parallelism and the subprocess_heavy marker by [@jlowin](https://github.com/jlowin) in [#4575](https://github.com/PrefectHQ/fastmcp/pull/4575) +* Document v3->v4 removals and add upgrade-reality tests by [@jlowin](https://github.com/jlowin) in [#4585](https://github.com/PrefectHQ/fastmcp/pull/4585) +* Archive v3 docs and publish v4 as the primary version by [@jlowin](https://github.com/jlowin) in [#4613](https://github.com/PrefectHQ/fastmcp/pull/4613) +* Document targeted v4 prerelease installation by [@zzstoatzz](https://github.com/zzstoatzz) in [#4598](https://github.com/PrefectHQ/fastmcp/pull/4598) +* Fix stale Mac/Windows-vs-Linux OAuth key/storage docs by [@jlowin](https://github.com/jlowin) in [#4617](https://github.com/PrefectHQ/fastmcp/pull/4617) +* v4 docs quality pass: stale task/era claims, broken links, polish by [@jlowin](https://github.com/jlowin) in [#4619](https://github.com/PrefectHQ/fastmcp/pull/4619) +* whats-new: add the argument completion capability by [@jlowin](https://github.com/jlowin) in [#4620](https://github.com/PrefectHQ/fastmcp/pull/4620) +* docs: fix ProxyProvider docstring example calling nonexistent with_namespace() by [@andrew-stelmach-fleet](https://github.com/andrew-stelmach-fleet) in [#4633](https://github.com/PrefectHQ/fastmcp/pull/4633) +* Unpublish v4 development notes; prep docs for beta 1 by [@jlowin](https://github.com/jlowin) in [#4644](https://github.com/PrefectHQ/fastmcp/pull/4644) +* Expand the FAQ for the v4 transition by [@jlowin](https://github.com/jlowin) in [#4649](https://github.com/PrefectHQ/fastmcp/pull/4649) +* Document the issuer_url identity change for upgraders by [@jlowin](https://github.com/jlowin) in [#4658](https://github.com/PrefectHQ/fastmcp/pull/4658) +* Cover require_roles in the v4 highlights by [@jlowin](https://github.com/jlowin) in [#4666](https://github.com/PrefectHQ/fastmcp/pull/4666) +* Fix FAQ: sampling/roots/elicitation legacy-mode advice, SessionProvider registration by [@jlowin](https://github.com/jlowin) in [#4672](https://github.com/PrefectHQ/fastmcp/pull/4672) +* Audit v4 docs: fix missing version badges, fill whats-new gaps by [@jlowin](https://github.com/jlowin) in [#4668](https://github.com/PrefectHQ/fastmcp/pull/4668) +* Docs: add v3.4.5 changelog entries to main by [@jlowin](https://github.com/jlowin) in [#4674](https://github.com/PrefectHQ/fastmcp/pull/4674) +* Split the SDK upgrade guides by SDK version by [@jlowin](https://github.com/jlowin) in [#4684](https://github.com/PrefectHQ/fastmcp/pull/4684) +### Dependencies 📦 +* chore(deps): bump mcp from 1.26.0 to 1.27.2 in /examples/testing_demo in the uv group across 1 directory by [@dependabot](https://github.com/apps/dependabot) in [#4514](https://github.com/PrefectHQ/fastmcp/pull/4514) +* chore(deps): bump actions/setup-node from 6 to 7 by [@dependabot](https://github.com/apps/dependabot) in [#4546](https://github.com/PrefectHQ/fastmcp/pull/4546) +* Bump actions/upload-artifact from 4 to 7 by [@dependabot](https://github.com/apps/dependabot) in [#4640](https://github.com/PrefectHQ/fastmcp/pull/4640) +* Bump actions/setup-python from 6 to 7 by [@dependabot](https://github.com/apps/dependabot) in [#4641](https://github.com/PrefectHQ/fastmcp/pull/4641) +* chore(deps): bump mcp from 1.27.2 to 1.28.1 in /examples/testing_demo in the uv group across 1 directory by [@dependabot](https://github.com/apps/dependabot) in [#4614](https://github.com/PrefectHQ/fastmcp/pull/4614) +### Other Changes 🦾 +* Test: HTTP lifespan fires once per process across sessions by [@jlowin](https://github.com/jlowin) in [#4470](https://github.com/PrefectHQ/fastmcp/pull/4470) +## New Contributors +* @syf2211 made their first contribution in [#4502](https://github.com/PrefectHQ/fastmcp/pull/4502) +* @earfman made their first contribution in [#4517](https://github.com/PrefectHQ/fastmcp/pull/4517) +* @winklemad made their first contribution in [#4492](https://github.com/PrefectHQ/fastmcp/pull/4492) +* @verdie-g made their first contribution in [#4512](https://github.com/PrefectHQ/fastmcp/pull/4512) +* @vijaydeepsinha made their first contribution in [#4411](https://github.com/PrefectHQ/fastmcp/pull/4411) +* @piaudonn made their first contribution in [#4267](https://github.com/PrefectHQ/fastmcp/pull/4267) +* @szepeviktor made their first contribution in [#4498](https://github.com/PrefectHQ/fastmcp/pull/4498) +* @endofcake made their first contribution in [#4412](https://github.com/PrefectHQ/fastmcp/pull/4412) +* @Kludex made their first contribution in [#4564](https://github.com/PrefectHQ/fastmcp/pull/4564) +* @RachGranville made their first contribution in [#4568](https://github.com/PrefectHQ/fastmcp/pull/4568) +* @hxaxd made their first contribution in [#4533](https://github.com/PrefectHQ/fastmcp/pull/4533) +* @VectorPeak made their first contribution in [#4531](https://github.com/PrefectHQ/fastmcp/pull/4531) +* @LarryHu0217 made their first contribution in [#4587](https://github.com/PrefectHQ/fastmcp/pull/4587) +* @andrew-stelmach-fleet made their first contribution in [#4633](https://github.com/PrefectHQ/fastmcp/pull/4633) +* @Aman071106 made their first contribution in [#4628](https://github.com/PrefectHQ/fastmcp/pull/4628) +* @ShuyingZhang made their first contribution in [#4639](https://github.com/PrefectHQ/fastmcp/pull/4639) +* @Educg550 made their first contribution in [#4660](https://github.com/PrefectHQ/fastmcp/pull/4660) + +**Full Changelog**: [v3.4.5...v4.0.0b1](https://github.com/PrefectHQ/fastmcp/compare/v3.4.5...v4.0.0b1) + + + **[v3.4.5: Key Change](https://github.com/PrefectHQ/fastmcp/releases/tag/v3.4.5)** diff --git a/docs/docs.json b/docs/docs.json index 7b560be71..417c30459 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -16,7 +16,7 @@ "dark": "#475569", "light": "#1e3a5f" }, - "content": "FastMCP 4 is in beta — you're reading the v4 docs. [What's new](/getting-started/whats-new) · [FastMCP 3 docs](/v3/getting-started/welcome)" + "content": "FastMCP 4 is in beta — check out [what's new](/getting-started/whats-new)!" }, "colors": { "dark": "#f72585", diff --git a/docs/getting-started/upgrading/from-fastmcp-3.mdx b/docs/getting-started/upgrading/from-fastmcp-3.mdx index 859c1d911..37ff33f62 100644 --- a/docs/getting-started/upgrading/from-fastmcp-3.mdx +++ b/docs/getting-started/upgrading/from-fastmcp-3.mdx @@ -5,7 +5,7 @@ description: What changes when you upgrade to FastMCP 4, which builds on the MCP icon: up --- -FastMCP 4 builds on the MCP Python SDK v2, and that is the source of every change in this guide. The SDK v2 makes two sweeping changes to the protocol layer: it moves the protocol types into a standalone `mcp_types` package (still importable as `mcp.types`), and it renames every protocol field from camelCase to snake_case (`inputSchema` → `input_schema`, `mimeType` → `mime_type`, `isError` → `is_error`, and so on). +FastMCP 4 builds on the MCP Python SDK v2, and that is the source of every change in this guide. The SDK v2 makes two sweeping changes to the protocol layer: it moves the protocol types into a standalone `mcp_types` package (still importable as `mcp.types`), and it renames every model field from camelCase to snake_case in Python (`inputSchema` → `input_schema`, `mimeType` → `mime_type`, `isError` → `is_error`, and so on). The wire format does not change: the models keep their camelCase aliases and serialize under them, so this renames the attributes your code reads, not the JSON on the connection. FastMCP 4 absorbs almost all of this for you. Field access is bridged so your existing reads keep working, and the imports you were taught have a stable home in FastMCP itself. What the SDK cannot hide is the protocol's own direction: the new sessionless era removes the server's ability to call back into a client mid-request, and background tasks moved out of the core spec into an extension. Those two shape the changes a working server is most likely to feel. diff --git a/docs/updates.mdx b/docs/updates.mdx index 47c0c8364..bb66069e9 100644 --- a/docs/updates.mdx +++ b/docs/updates.mdx @@ -5,6 +5,28 @@ icon: "sparkles" tag: NEW --- + + +FastMCP 4 rebuilds the framework on the MCP Python SDK v2, and this beta is the first release to run on the SDK's stable 2.0. The engine underneath changed completely, but FastMCP absorbs nearly all of it — most FastMCP 3 servers run untouched. + +🌐 **Every protocol era** — one server answers both the sessionless `2026-07-28` protocol and the older session-based handshake, negotiated per connection. + +💾 **State without a session** — `UserSession` and `SessionId` give tools durable state on a protocol that deliberately has none, keyed per user when the request is authenticated. + +⏳ **Background tasks** — the `io.modelcontextprotocol/tasks` extension in the new `fastmcp-tasks` package, on the same Docket engine FastMCP 3 used. + +🧩 **Server extensions** — `add_extension()` turns capability-negotiated protocol features into a supported plugin surface. + +🔐 **Enterprise auth** — server-side identity assertion (SEP-990), `require_roles`, scope step-up challenges, and DCR `application_type`. + +⚠️ **Breaking** — server-initiated sampling and roots are removed from the server API, and the 3.x-era compatibility shims are gone. See the [upgrade guide](/getting-started/upgrading/from-fastmcp-3). + + + Date: Tue, 28 Jul 2026 17:30:20 -0400 Subject: [PATCH 16/53] Always emit a tool title, derived from name when unset (#4694) * Always emit a tool title, derived from name when unset Some MCP clients (e.g. ChatGPT) drop tools with no `title` instead of falling back to `name` for display as the spec allows. Deriving a default title in Tool.to_mcp_tool() fixes this for every tool built on top of it, including the search-transform, code-mode, and session proxy tools that never set one explicitly. Fixes #4414 * Derive fallback title from the overridden name, document it Addresses Codex review on #4694. * Resolve title precedence from effective overrides * Normalize mapping annotations before deriving the title --- docs/servers/tools.mdx | 2 +- fastmcp_slim/fastmcp/tools/base.py | 27 +++++++++++++--- tests/server/transforms/test_search.py | 22 +++++++++++++ tests/tools/tool/test_title.py | 44 +++++++++++++++++++++++--- 4 files changed, 84 insertions(+), 11 deletions(-) diff --git a/docs/servers/tools.mdx b/docs/servers/tools.mdx index 527b16b9b..f7fe31ec0 100644 --- a/docs/servers/tools.mdx +++ b/docs/servers/tools.mdx @@ -74,7 +74,7 @@ def search_products_implementation(query: str, category: str | None = None) -> l - A human-readable display title for the tool. If omitted, FastMCP falls back to `annotations.title` when present. + A human-readable display title for the tool. If omitted, FastMCP falls back to `annotations.title` when present, then to a title derived from the tool's name (e.g. `find_products` becomes "Find Products") — some MCP clients drop tools that have no title at all. diff --git a/fastmcp_slim/fastmcp/tools/base.py b/fastmcp_slim/fastmcp/tools/base.py index ad21f1253..5e02246dd 100644 --- a/fastmcp_slim/fastmcp/tools/base.py +++ b/fastmcp_slim/fastmcp/tools/base.py @@ -52,6 +52,16 @@ if TYPE_CHECKING: logger = get_logger(__name__) +def _default_title(name: str) -> str: + """Derive a display title from a tool name. + + The MCP spec says clients should fall back to `name` for display when + `title` is absent, but some clients (e.g. ChatGPT) instead drop the tool + entirely. Always emitting a title avoids depending on that fallback. + """ + return name.replace("_", " ").replace("-", " ").title() + + def resolve_serialize_by_alias(value: Any) -> bool: """Resolve the effective ``by_alias`` setting for serializing *value*. @@ -263,21 +273,28 @@ class Tool(FastMCPComponent): **overrides: Any, ) -> MCPTool: """Convert the FastMCP tool to an MCP tool.""" - title = None + # Title precedence follows the effective (post-override) values, so a + # caller renaming or re-annotating a tool doesn't get a stale title. + name = overrides.get("name", self.name) + annotations = overrides.get("annotations", self.annotations) + if isinstance(annotations, dict): + annotations = ToolAnnotations(**annotations) if self.title: title = self.title - elif self.annotations and self.annotations.title: - title = self.annotations.title + elif annotations and annotations.title: + title = annotations.title + else: + title = _default_title(name) mcp_tool = MCPTool( - name=overrides.get("name", self.name), + name=name, title=overrides.get("title", title), description=overrides.get("description", self.description), input_schema=overrides.get("inputSchema", self.parameters), output_schema=overrides.get("outputSchema", self.output_schema), icons=overrides.get("icons", self.icons), - annotations=overrides.get("annotations", self.annotations), + annotations=annotations, execution=overrides.get("execution", self.execution), _meta=overrides.get( # type: ignore[call-arg] # _meta is Pydantic alias for meta field "_meta", self.get_meta() diff --git a/tests/server/transforms/test_search.py b/tests/server/transforms/test_search.py index 9c508f893..810a05bdd 100644 --- a/tests/server/transforms/test_search.py +++ b/tests/server/transforms/test_search.py @@ -137,6 +137,28 @@ class TestBaseTransformBehavior: assert await mcp.get_tool("find_tools") is not None assert await mcp.get_tool("run_tool") is not None + @pytest.mark.parametrize( + "transform_cls", [RegexSearchTransform, BM25SearchTransform] + ) + async def test_synthetic_tools_have_titles(self, transform_cls): + """Synthetic search/call tools must carry a title. + + Some MCP clients (e.g. ChatGPT) drop tools with no `title` field, + which breaks tool-search discovery entirely. See #4414. + """ + mcp = _make_server_with_tools() + mcp.add_transform( + transform_cls( + search_tool_name="find_tools", call_tool_name="call_read_tool" + ) + ) + tools = await mcp.list_tools() + titles = {t.name: t.to_mcp_tool().title for t in tools} + assert titles == { + "find_tools": "Find Tools", + "call_read_tool": "Call Read Tool", + } + async def test_search_respects_visibility_filtering(self): """Tools disabled via Visibility transform should not appear in search.""" mcp = _make_server_with_tools() diff --git a/tests/tools/tool/test_title.py b/tests/tools/tool/test_title.py index 29c0c1a73..fde193f70 100644 --- a/tests/tools/tool/test_title.py +++ b/tests/tools/tool/test_title.py @@ -1,3 +1,6 @@ +import pytest +from mcp_types import ToolAnnotations + from fastmcp.tools.base import Tool @@ -30,7 +33,13 @@ class TestToolTitle: ) def test_tool_without_title(self): - """Test that tools without titles use name as display name.""" + """Test that tools without an explicit title derive one from the name. + + Some MCP clients (e.g. ChatGPT) drop tools with no `title` rather + than falling back to `name` as the spec allows, so FastMCP always + emits a derived title on the wire instead of relying on that + fallback. + """ def multiply(a: int, b: int) -> int: return a * b @@ -40,14 +49,40 @@ class TestToolTitle: assert tool.name == "multiply" assert tool.title is None - # Test MCP conversion doesn't include title when None mcp_tool = tool.to_mcp_tool() assert mcp_tool.name == "multiply" - assert not hasattr(mcp_tool, "title") or mcp_tool.title is None + assert mcp_tool.title == "Multiply" + + def test_derived_title_follows_name_override(self): + """The derived title should reflect a `name` override, not the original name.""" + + def multiply(a: int, b: int) -> int: + return a * b + + tool = Tool.from_function(multiply, name="multiply_tool") + + mcp_tool = tool.to_mcp_tool(name="renamed_tool") + assert mcp_tool.name == "renamed_tool" + assert mcp_tool.title == "Renamed Tool" + + @pytest.mark.parametrize( + "annotations", + [ToolAnnotations(title="Custom"), {"title": "Custom"}], + ids=["object", "dict"], + ) + def test_annotations_override_beats_derived_title(self, annotations): + """An `annotations` override still outranks the name-derived title.""" + + def multiply(a: int, b: int) -> int: + return a * b + + tool = Tool.from_function(multiply) + + mcp_tool = tool.to_mcp_tool(annotations=annotations) + assert mcp_tool.title == "Custom" def test_tool_title_priority(self): """Test that explicit title takes priority over annotations.title.""" - from mcp_types import ToolAnnotations def divide(x: int, y: int) -> float: """Divide two numbers.""" @@ -72,7 +107,6 @@ class TestToolTitle: def test_tool_annotations_title_fallback(self): """Test that annotations.title is used when no explicit title is provided.""" - from mcp_types import ToolAnnotations def modulo(x: int, y: int) -> int: """Get modulo of two numbers.""" From 7a77805159a7833520c00236a243c54023e5e0e9 Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Tue, 28 Jul 2026 19:08:44 -0400 Subject: [PATCH 17/53] Use mcp.types directly instead of aliasing to mcp_types in docs (#4696) --- docs/clients/notifications.mdx | 30 +++++++++++++++--------------- docs/servers/context.mdx | 14 +++++++++----- docs/servers/tasks.mdx | 17 +++++++++++------ 3 files changed, 35 insertions(+), 26 deletions(-) diff --git a/docs/clients/notifications.mdx b/docs/clients/notifications.mdx index 1864dd23b..b771c903e 100644 --- a/docs/clients/notifications.mdx +++ b/docs/clients/notifications.mdx @@ -47,23 +47,23 @@ For fine-grained targeting, subclass `MessageHandler` to use specific hooks: ```python from fastmcp import Client from fastmcp.client.messages import MessageHandler -import mcp.types as mcp_types +import mcp.types class MyMessageHandler(MessageHandler): async def on_tool_list_changed( - self, notification: mcp_types.ToolListChangedNotification + self, notification: mcp.types.ToolListChangedNotification ) -> None: """Handle tool list changes.""" print("Tool list changed - refreshing available tools") async def on_resource_list_changed( - self, notification: mcp_types.ResourceListChangedNotification + self, notification: mcp.types.ResourceListChangedNotification ) -> None: """Handle resource list changes.""" print("Resource list changed") async def on_prompt_list_changed( - self, notification: mcp_types.PromptListChangedNotification + self, notification: mcp.types.PromptListChangedNotification ) -> None: """Handle prompt list changes.""" print("Prompt list changed") @@ -78,7 +78,7 @@ client = Client( ```python from fastmcp.client.messages import MessageHandler -import mcp.types as mcp_types +import mcp.types class MyMessageHandler(MessageHandler): async def on_message(self, message) -> None: @@ -86,49 +86,49 @@ class MyMessageHandler(MessageHandler): pass async def on_notification( - self, notification: mcp_types.ServerNotification + self, notification: mcp.types.ServerNotification ) -> None: """Called for notifications (fire-and-forget).""" pass async def on_tool_list_changed( - self, notification: mcp_types.ToolListChangedNotification + self, notification: mcp.types.ToolListChangedNotification ) -> None: """Called when the server's tool list changes.""" pass async def on_resource_list_changed( - self, notification: mcp_types.ResourceListChangedNotification + self, notification: mcp.types.ResourceListChangedNotification ) -> None: """Called when the server's resource list changes.""" pass async def on_prompt_list_changed( - self, notification: mcp_types.PromptListChangedNotification + self, notification: mcp.types.PromptListChangedNotification ) -> None: """Called when the server's prompt list changes.""" pass async def on_progress( - self, notification: mcp_types.ProgressNotification + self, notification: mcp.types.ProgressNotification ) -> None: """Called for progress updates during long-running operations.""" pass async def on_resource_updated( - self, notification: mcp_types.ResourceUpdatedNotification + self, notification: mcp.types.ResourceUpdatedNotification ) -> None: """Called when a specific resource changes.""" pass async def on_cancelled( - self, notification: mcp_types.CancelledNotification + self, notification: mcp.types.CancelledNotification ) -> None: """Called when a request is cancelled.""" pass async def on_logging_message( - self, notification: mcp_types.LoggingMessageNotification + self, notification: mcp.types.LoggingMessageNotification ) -> None: """Called for log messages from the server.""" pass @@ -141,14 +141,14 @@ A practical example of maintaining a tool cache that refreshes when tools change ```python from fastmcp import Client from fastmcp.client.messages import MessageHandler -import mcp.types as mcp_types +import mcp.types class ToolCacheHandler(MessageHandler): def __init__(self): self.cached_tools = [] async def on_tool_list_changed( - self, notification: mcp_types.ToolListChangedNotification + self, notification: mcp.types.ToolListChangedNotification ) -> None: """Clear tool cache when tools change.""" print("Tools changed - clearing cache") diff --git a/docs/servers/context.mdx b/docs/servers/context.mdx index 01a5372d3..667ce9764 100644 --- a/docs/servers/context.mdx +++ b/docs/servers/context.mdx @@ -179,7 +179,7 @@ content = resource_result.contents[0].content ``` **Method signatures:** -- **`ctx.list_resources() -> list[mcp_types.Resource]`**: Returns list of all available resources +- **`ctx.list_resources() -> list[mcp.types.Resource]`**: Returns list of all available resources - **`ctx.read_resource(uri: str | AnyUrl) -> ResourceResult`**: Returns a `ResourceResult` whose `.contents` list contains the resource content parts ### Prompt Access @@ -271,14 +271,18 @@ Tools can customize which components are visible to their current session using FastMCP automatically sends list change notifications when components (such as tools, resources, or prompts) are added, removed, enabled, or disabled. In rare cases where you need to manually trigger these notifications, you can use the context's notification methods: ```python -import mcp.types as mcp_types +from mcp.types import ( + PromptListChangedNotification, + ResourceListChangedNotification, + ToolListChangedNotification, +) @mcp.tool async def custom_tool_management(ctx: Context) -> str: """Example of manual notification after custom tool changes.""" - await ctx.send_notification(mcp_types.ToolListChangedNotification()) - await ctx.send_notification(mcp_types.ResourceListChangedNotification()) - await ctx.send_notification(mcp_types.PromptListChangedNotification()) + await ctx.send_notification(ToolListChangedNotification()) + await ctx.send_notification(ResourceListChangedNotification()) + await ctx.send_notification(PromptListChangedNotification()) return "Notifications sent" ``` diff --git a/docs/servers/tasks.mdx b/docs/servers/tasks.mdx index 2b07bd440..b22a7b91d 100644 --- a/docs/servers/tasks.mdx +++ b/docs/servers/tasks.mdx @@ -225,30 +225,35 @@ A tool can ask the client a question partway through — the same [guard pattern ```python from fastmcp import Context, FastMCP from fastmcp_tasks import TasksExtension -import mcp.types as mcp_types +from mcp.types import ( + ElicitRequest, + ElicitRequestFormParams, + ElicitResult, + InputRequiredResult, +) mcp = FastMCP("MyServer") mcp.add_extension(TasksExtension()) @mcp.tool(task=True) -async def plan_dinner(ctx: Context) -> str | mcp_types.InputRequiredResult: +async def plan_dinner(ctx: Context) -> str | InputRequiredResult: responses = ctx.input_responses if responses is None: # First leg: ask a question and end here. - request = mcp_types.ElicitRequest( - params=mcp_types.ElicitRequestFormParams( + request = ElicitRequest( + params=ElicitRequestFormParams( message="What are you in the mood for?", requested_schema={"type": "object", "properties": {"cuisine": {"type": "string"}}}, ) ) - return mcp_types.InputRequiredResult( + return InputRequiredResult( result_type="input_required", input_requests={"prefs": request}, ) # Re-entered leg: the client's answer is on ctx.input_responses. answer = responses["prefs"] - assert isinstance(answer, mcp_types.ElicitResult) + assert isinstance(answer, ElicitResult) return f"Tonight: {answer.content['cuisine']}!" ``` From 73399369806fc19a328376b0ded181c319c31591 Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Tue, 28 Jul 2026 20:05:45 -0400 Subject: [PATCH 18/53] Rewrite the v4 What's New page and document server extensions (#4698) * Rewrite the v4 What's New page Teach the headline features with code instead of asserting them, drop the major-version throat-clearing and SEP list, and correct the elicitation claim: ctx.elicit() is unchanged and handshake-only, while sampling and roots are removed outright. * Fix broken doc links and stale version references Repoint five dead links and anchors, refresh v3-era version examples on the v4 docs, and add the missing FastMCP 3 entry to the installation page's upgrade section. * Document server extensions add_extension() shipped in v4 with no documentation page. Covers the extension interface, request methods, tool-call interception, lifespan ownership, and the client half. * Link the FastMCP TypeScript library * Address Codex review feedback Gate the extension interceptor on the client's per-request opt-in rather than claiming negotiation does it; show the v4 beta pin on the install page instead of a version a reader cannot get; note that UserSession requires authentication. --- README.md | 3 + docs/deployment/http.mdx | 4 +- docs/development/releases.mdx | 4 +- docs/docs.json | 1 + docs/getting-started/installation.mdx | 32 ++-- docs/getting-started/quickstart.mdx | 7 +- .../upgrading/from-fastmcp-2.mdx | 2 +- docs/getting-started/welcome.mdx | 2 + docs/getting-started/whats-new.mdx | 126 +++++++++---- docs/more/faq.mdx | 2 +- docs/servers/extensions.mdx | 166 ++++++++++++++++++ docs/servers/providers/overview.mdx | 10 +- docs/servers/storage-backends.mdx | 4 +- docs/servers/tools.mdx | 2 +- docs/servers/transforms/code-mode.mdx | 6 +- 15 files changed, 301 insertions(+), 70 deletions(-) create mode 100644 docs/servers/extensions.mdx diff --git a/README.md b/README.md index e29ce7a9b..5d312c0b3 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ [![Docs](https://img.shields.io/badge/docs-gofastmcp.com-blue)](https://gofastmcp.com) [![Discord](https://img.shields.io/badge/community-discord-5865F2?logo=discord&logoColor=white)](https://discord.gg/uu8dJCgttd) [![PyPI - Version](https://img.shields.io/pypi/v/fastmcp.svg)](https://pypi.org/project/fastmcp) +[![TypeScript](https://img.shields.io/npm/v/%40prefecthq%2Ffastmcp-ts?label=typescript&color=3178c6)](https://github.com/PrefectHQ/fastmcp-ts) [![Tests](https://github.com/PrefectHQ/fastmcp/actions/workflows/run-tests.yml/badge.svg)](https://github.com/PrefectHQ/fastmcp/actions/workflows/run-tests.yml) [![License](https://img.shields.io/github/license/PrefectHQ/fastmcp.svg)](https://github.com/PrefectHQ/fastmcp/blob/main/LICENSE) @@ -77,6 +78,8 @@ FastMCP has three pillars: **[Servers](https://gofastmcp.com/servers/server)** wrap your Python functions into MCP-compliant tools, resources, and prompts. **[Clients](https://gofastmcp.com/clients/client)** connect to any server with full protocol support. And **[Apps](https://gofastmcp.com/apps/overview)** give your tools interactive UIs rendered directly in the conversation. +**Building in TypeScript?** [FastMCP for TypeScript](https://github.com/PrefectHQ/fastmcp-ts) is the official counterpart, built and maintained by the same team. Same pillars, same ideas, `npm install @prefecthq/fastmcp-ts`. + Ready to build? Start with the [installation guide](https://gofastmcp.com/getting-started/installation) or jump straight to the [quickstart](https://gofastmcp.com/getting-started/quickstart). ## Run FastMCP in production with Horizon diff --git a/docs/deployment/http.mdx b/docs/deployment/http.mdx index 29d104d4e..f057efe4c 100644 --- a/docs/deployment/http.mdx +++ b/docs/deployment/http.mdx @@ -103,7 +103,7 @@ If you're mounting an authenticated server under a path prefix, see [Mounting Au ### Host and Origin Protection -FastMCP can validate `Host` and browser `Origin` headers for Streamable HTTP requests before they reach MCP session handling. This request guard protects localhost-bound servers from DNS rebinding attacks, and it remains opt-in in FastMCP 3.x to preserve compatibility with existing ASGI, serverless, and reverse-proxy deployments. +FastMCP can validate `Host` and browser `Origin` headers for Streamable HTTP requests before they reach MCP session handling. This request guard protects localhost-bound servers from DNS rebinding attacks, and it stays opt-in to preserve compatibility with existing ASGI, serverless, and reverse-proxy deployments. Think of this as a request guard rather than CORS middleware. It decides whether a request can reach MCP session handling. CORS remains a separate browser response-header policy; configure CORS middleware separately when browser JavaScript must read cross-origin responses. @@ -188,7 +188,7 @@ def query_tenant( A gateway can now route on `Mcp-Param-Tenant` — for example, pinning each tenant to a dedicated backend — without inspecting the request body. The annotation is only permitted on `string`, `integer`, and `boolean` parameters. These headers advertise routing intent; treat them as untrusted hints, since the server still validates the request body as the source of truth. -When you put a FastMCP [proxy](/servers/proxy) in front of another server, the proxy re-advertises each backend tool's `x-mcp-header` annotation, so routing headers work across the proxy hop as well. The headers themselves are regenerated per hop rather than forwarded verbatim, since each describes a single HTTP request. +When you put a FastMCP [proxy](/servers/providers/proxy) in front of another server, the proxy re-advertises each backend tool's `x-mcp-header` annotation, so routing headers work across the proxy hop as well. The headers themselves are regenerated per hop rather than forwarded verbatim, since each describes a single HTTP request. ### Health Checks diff --git a/docs/development/releases.mdx b/docs/development/releases.mdx index 331fd810c..ecba9a25f 100644 --- a/docs/development/releases.mdx +++ b/docs/development/releases.mdx @@ -53,8 +53,8 @@ We expect this exemption to last through at least the 2.12.x and 2.13.x release Pin to exact versions: ``` -fastmcp==2.11.0 # Good -fastmcp>=2.11.0 # Bad - will install breaking changes +fastmcp==4.0.0 # Good +fastmcp>=4.0.0 # Bad - will install breaking changes ``` ## Creating Releases diff --git a/docs/docs.json b/docs/docs.json index 417c30459..22bc3c4b9 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -162,6 +162,7 @@ "servers/lifespan", "servers/storage-backends", "servers/sessions", + "servers/extensions", "servers/tasks", "servers/versioning" ] diff --git a/docs/getting-started/installation.mdx b/docs/getting-started/installation.mdx index 9bfbdbcb1..c3c1bdacd 100644 --- a/docs/getting-started/installation.mdx +++ b/docs/getting-started/installation.mdx @@ -7,15 +7,19 @@ icon: arrow-down-to-line We recommend using [uv](https://docs.astral.sh/uv/getting-started/installation/) to install and manage FastMCP. +```bash +uv add fastmcp +``` + +Or with pip: + ```bash pip install fastmcp ``` -Or with uv: - -```bash -uv add fastmcp -``` + +**FastMCP 4 is in prerelease.** The commands above install the latest stable release, which is still 3.x. To get v4, pin the beta explicitly with `pip install "fastmcp==4.0.0b1"`, or see [Install the v4 Prerelease](/getting-started/upgrading/from-fastmcp-3#install-the-v4-prerelease) for the uv constraint you'll need. + ### Optional Dependencies @@ -40,8 +44,8 @@ You should see output like the following: ```bash $ fastmcp version -FastMCP version: 3.0.0 -MCP version: 1.25.0 +FastMCP version: 4.0.0b1 +MCP version: 2.0.0 Python version: 3.12.2 Platform: macOS-15.3.1-arm64-arm-64bit FastMCP root path: ~/Developer/fastmcp @@ -62,6 +66,10 @@ Alternatively, wait for the stable v5 release. See [this issue](https://github.c ## Upgrading +### From FastMCP 3.0 + +Most FastMCP 3 servers run on 4 without changes. See [Upgrading from FastMCP 3](/getting-started/upgrading/from-fastmcp-3) for the breaks that do exist, and [What's New](/getting-started/whats-new) for what the new version adds. + ### From FastMCP 2.0 See the [Upgrade Guide](/getting-started/upgrading/from-fastmcp-2) for a complete list of breaking changes and migration steps. @@ -107,16 +115,12 @@ FastMCP follows semantic versioning with pragmatic adaptations for the rapidly e For production use, always pin to exact versions: ``` -fastmcp==3.0.0 # Good -fastmcp>=3.0.0 # Bad - may install breaking changes +fastmcp==4.0.0 # Good +fastmcp>=4.0.0 # Bad - may install breaking changes ``` See the full [versioning and release policy](/development/releases#versioning-policy) for details on our public API, deprecation practices, and breaking change philosophy. ## Contributing to FastMCP -Interested in contributing to FastMCP? See the [Contributing Guide](/development/contributing) for details on: -- Setting up your development environment -- Running tests and pre-commit hooks -- Submitting issues and pull requests -- Code standards and review process +The [Contributing Guide](/development/contributing) covers setting up a development environment, running the test suite and pre-commit hooks, and the standards we hold contributed code to. diff --git a/docs/getting-started/quickstart.mdx b/docs/getting-started/quickstart.mdx index 97d9f3c79..ae157d906 100644 --- a/docs/getting-started/quickstart.mdx +++ b/docs/getting-started/quickstart.mdx @@ -3,7 +3,7 @@ title: Quickstart icon: rocket-launch --- -Welcome! This guide will help you quickly set up FastMCP, run your first MCP server, give it a visual UI, and deploy it to Prefect Horizon. +This guide builds a working MCP server from scratch: a tool, a way to run it, a client that calls it, and a visual UI for the result. It ends with the server deployed and reachable over the internet. If you haven't already installed FastMCP, follow the [installation instructions](/getting-started/installation). @@ -112,10 +112,7 @@ async def call_tool(name: str): asyncio.run(call_tool("Ford")) ``` -Note that: -- FastMCP clients are asynchronous, so we need to use `asyncio.run` to run the client -- We must enter a client context (`async with client:`) before using the client -- You can make multiple client calls within the same context +FastMCP clients are asynchronous, so the call goes through `asyncio.run`. Entering the client context with `async with client:` is what opens the connection, and it stays open for as many calls as you want to make inside the block. ## Give Your Tool a UI diff --git a/docs/getting-started/upgrading/from-fastmcp-2.mdx b/docs/getting-started/upgrading/from-fastmcp-2.mdx index 98f0b4883..c371f09f6 100644 --- a/docs/getting-started/upgrading/from-fastmcp-2.mdx +++ b/docs/getting-started/upgrading/from-fastmcp-2.mdx @@ -25,7 +25,7 @@ pip install --upgrade fastmcp uv add --upgrade fastmcp ``` -If you pin versions in a requirements file or `pyproject.toml`, update your pin to `fastmcp>=3.0.0,<4`. +If you pin versions in a requirements file or `pyproject.toml`, update your pin to `fastmcp>=3.0.0,<4`. Going on to FastMCP 4 is a second hop: finish this page, then work through [Upgrading from FastMCP 3](/getting-started/upgrading/from-fastmcp-3) and move the pin to `fastmcp>=4.0.0` at the end of it. **New repository home.** As part of the v3 release, FastMCP's GitHub repository has moved from `jlowin/fastmcp` to [`PrefectHQ/fastmcp`](https://github.com/PrefectHQ/fastmcp) under [Prefect](https://prefect.io)'s stewardship. GitHub automatically redirects existing clones and bookmarks, so nothing breaks — but you can update your local remote whenever convenient: diff --git a/docs/getting-started/welcome.mdx b/docs/getting-started/welcome.mdx index d42dc39b7..1e3fe2a9d 100644 --- a/docs/getting-started/welcome.mdx +++ b/docs/getting-started/welcome.mdx @@ -80,6 +80,8 @@ FastMCP has three pillars: **[Servers](/servers/server)** wrap your Python functions into MCP-compliant tools, resources, and prompts. **[Clients](/clients/client)** connect to any server with full protocol support. And **[Apps](/apps/overview)** give your tools interactive UIs rendered directly in the conversation. +**Building in TypeScript?** [FastMCP for TypeScript](https://github.com/PrefectHQ/fastmcp-ts) is the official counterpart, built and maintained by the same team. The three pillars work the same way there, so what you learn here carries over. + Ready to build? Start with the [installation guide](/getting-started/installation) or jump straight to the [quickstart](/getting-started/quickstart). FastMCP is made with 💙 by [Prefect](https://www.prefect.io/). diff --git a/docs/getting-started/whats-new.mdx b/docs/getting-started/whats-new.mdx index b2dfffb8d..230bb5844 100644 --- a/docs/getting-started/whats-new.mdx +++ b/docs/getting-started/whats-new.mdx @@ -1,51 +1,114 @@ --- title: "What's New in FastMCP 4" sidebarTitle: "What's New" -description: The capabilities that define FastMCP 4 — a rebuilt engine, a new protocol era, and a stateless protocol made practical. +description: A sessionless MCP protocol, the state layer that replaces sessions, and enterprise identity. icon: sparkles --- -FastMCP 4 is a major version because its engine changed. The framework is now built on the MCP Python SDK v2, a ground-up rebuild of the protocol layer, and on that foundation it adds a new protocol era, first-class extensions, stateless state, enterprise identity, and more. Most FastMCP 3 servers run on it untouched — the major version signals how much moved underneath, and what that movement unlocks. +FastMCP 4 runs on version 2 of the MCP Python SDK, which rewrote the protocol layer to support MCP's new sessionless protocol, `2026-07-28`. That protocol drives most of this release. It changes how servers deploy, how clients connect, where state lives between calls, and how a running tool asks the user a question. + +Most FastMCP 3 servers run on 4 unchanged. Two things need attention: `ctx.sample()` and `ctx.list_roots()` are gone, and code that builds MCP protocol models by hand now uses snake_case field names where the SDK used camelCase. [Upgrading from FastMCP 3](/getting-started/upgrading/from-fastmcp-3) covers every break in detail. FastMCP 4 is in **beta**. Pin an exact version and expect sharp edges. See [Install the v4 prerelease](/getting-started/upgrading/from-fastmcp-3#install-the-v4-prerelease). -## Built on the MCP Python SDK v2 - -The defining change in FastMCP 4 is the one you mostly can't see. The MCP Python SDK v2 rewrote the protocol layer end to end: it moved the protocol types into a standalone `mcp_types` package that stays importable as `mcp.types`, renamed every model field from camelCase to snake_case in Python, replaced the server's request-handling model, and made server-side middleware and multi-era serving first-class. FastMCP absorbs nearly all of it — your reads stay working through a compatibility bridge, and the handful of changes left in your code are mechanical. - -The major version is the signal. Even where your surface is unchanged, the behavior underneath is substantially different, and bumping to 4.0 is how we tell you that plainly rather than slipping a new engine in under a patch release. - -The rebuild also pulls the protocol's recent evolution forward in a single step. A batch of accepted MCP proposals arrives with SDK v2, and FastMCP 4 surfaces each one: capability-negotiated extensions (SEP-2133), multi-round-trip elicitation for sessionless connections (SEP-2322), response cache hints (SEP-2549), spec-standard error codes (SEP-2164), the enterprise identity-assertion grant (SEP-990), and the sessionless `2026-07-28` protocol itself, which removes server-initiated requests (SEP-2577). The rest of this page is what those add up to. - ## Every protocol era -A FastMCP 4 server answers clients across the protocol transition from one deployment. The MCP SDK negotiates the era per connection — the sessionless `2026-07-28` protocol for clients that have moved forward, the session-based handshake for everyone else — and any replica behind a plain load balancer can serve a modern request. This supersedes FastMCP's earlier "latest protocol only" stance: you adopt the new protocol without forking your deployment or gating clients by version. +A FastMCP 4 server answers clients on both sides of the protocol transition from a single deployment. The SDK negotiates per connection: the sessionless protocol for clients that have moved forward, the session-based handshake for everyone else. You adopt the new protocol without forking your deployment or gating clients by version, which supersedes FastMCP's earlier "latest protocol only" stance. -The same negotiation runs from the client, and its default flipped. A plain `Client(url)` now probes for the modern protocol and adopts it when the server offers it, falling back to the handshake otherwise — where every earlier FastMCP version pinned the handshake outright. That flip is what brings the modern capabilities within reach of ordinary client code: a task-enabled tool hands back a handle to poll, and multi-round-trip elicitation resolves across successive requests, neither requiring the caller to opt in. Set `mode="legacy"` to pin the handshake when you need the session-based back-channel or the classic `initialize` result. Once connected, `client.protocol_version`, `client.server_info`, `client.server_capabilities`, and `client.instructions` read the same regardless of which era you negotiated — code that inspects the connection no longer branches on how it got there. See [Protocol negotiation](/clients/client#protocol-negotiation). +Statelessness pays off in how you run the server. A sessionless request carries everything needed to answer it, so any replica behind an ordinary load balancer can serve any request and session affinity stops being a deployment requirement. -The modern protocol is sessionless, so it drops the server's ability to call back into the client mid-request (SEP-2577), and FastMCP 4's server API reflects that. `ctx.elicit` moves to a request-shaped pattern that works on modern connections: the tool returns a description of the input it needs, and the client answers with a fresh call. `ctx.sample`, `ctx.sample_step`, and `ctx.list_roots` are gone from the API, because each of them pushed a request down a live connection and a method that only works against old clients is a trap. +The client default flipped to match. `Client(url)` probes for the modern protocol and adopts it when the server offers it, where every earlier FastMCP version pinned the handshake outright. -Both capabilities survive in the same request-shaped form. Asking for roots that way is the natural replacement, since one round trip buys the whole answer. Generation usually belongs in the server instead, because a loop of asking rounds spends the round-trip budget over and over — [call an LLM from your server](/servers/sampling). Logging is untouched: `ctx.info` and its siblings are notifications, and notifications ride the response stream on every era. Everything else about writing a server is unchanged. +```python +from fastmcp import Client -## State without a session +# Probes for the modern protocol, falls back to the handshake +client = Client("https://example.com/mcp") -A stateless protocol raises an obvious question: if every request is a fresh connection, where does a tool keep a shopping cart, a conversation, or a running total? FastMCP 4 follows the MCP working group's own decision to reject protocol-level sessions in favor of *explicit state handles* (SEP-2567) — the server hands out an identifier, and the client passes it back. +# Pins the handshake, when you need the session back-channel +legacy = Client("https://example.com/mcp", mode="legacy") +``` -Two shapes cover the cases. `UserSession` is injected like `Context` and keyed to the authenticated user, so a tool reads and writes one bucket of state with nothing to pass around. `SessionId` is an explicit handle a tool mints and the caller supplies as an argument, for when one user holds many independent states. Both store their data server-side in the storage backend, keyed to the authenticated user — so a handle is inert in anyone else's hands. See [Session State](/servers/sessions). +That default is what puts the modern capabilities within reach of ordinary client code: a task-enabled tool hands back a handle to poll, and multi-round-trip elicitation resolves across successive requests, with the caller opting in to neither. Once connected, `client.protocol_version`, `client.server_info`, `client.server_capabilities`, and `client.instructions` read the same whichever era you negotiated, so code that inspects a connection never branches on how it was established. See [Protocol negotiation](/clients/client#protocol-negotiation). + +Intermediaries benefit too. On a modern connection, FastMCP's client attaches the method, the target name, and any opted-in argument values as HTTP headers, so a gateway or load balancer can route a request without parsing its JSON-RPC body. See [Gateway Routing Headers](/deployment/http#gateway-routing-headers). + +## Server-to-client requests + +A sessionless connection gives the server no channel to push a request down to a connected client mid-execution. Three `Context` methods depended on that channel, and this is the one part of FastMCP 4 likely to break an existing server. + +`ctx.sample()`, `ctx.sample_step()`, and `ctx.list_roots()` are removed. Touching one raises `AttributeError` on every era, so the break surfaces when you upgrade rather than in production against whichever client happens to negotiate the modern protocol. + +For generation, call an LLM directly from your tool: your server holds the API key, creates a provider client, and awaits a completion inline. That works against every client, including the many that never implemented sampling at all, and a tool that chains several generations pays no round trip for any of them. See [Sampling](/servers/sampling). + +When borrowing the *caller's* model is the actual point, or when a tool genuinely needs the client's roots, the tool asks by returning a description of what it needs. The round completes normally, the client answers, and it re-issues the call with the answer attached. `ctx.elicit()` is untouched and still works on handshake connections; on modern connections that same return-and-resume shape covers elicitation as well. See [the guard pattern](/servers/elicitation#elicitation-on-the-modern-protocol). + +Logging and progress are unaffected. Both are notifications, and notifications ride the response stream on every era. + +## Session state + +If every request arrives on a fresh connection, a tool that wants to remember something between calls has nowhere to keep it. Weighing protocol-level sessions against statelessness, the MCP working group [chose statelessness](https://github.com/modelcontextprotocol/transports-wg/blob/main/docs/sessions-vs-sessionless-decision.md) and moved session semantics up to the application: the server hands out an identifier, and the client passes it back. + +FastMCP implements that pattern and adds the isolation a bare handle lacks. State is stored server-side and keyed to the authenticated user, so a handle is inert in anyone else's hands. + +Most tools want a single bucket per user. Declare a `UserSession` parameter and FastMCP injects it the way it injects `Context`: it never appears in the tool's input schema, and the caller passes nothing, because the user's identity selects the right bucket. + +```python +from fastmcp import FastMCP +from fastmcp.server.sessions import UserSession + +mcp = FastMCP("assistant") + + +@mcp.tool +async def remember(fact: str, session: UserSession) -> str: + facts = await session.get("facts", default=[]) + facts.append(fact) + await session.set("facts", facts) + return f"Remembered {len(facts)} facts." +``` + +Because the bucket is chosen from the caller's identity, `UserSession` requires [authentication](/servers/auth/authentication). An unauthenticated request has no user to key on, so the tool raises rather than guessing at a bucket. + +When one user needs several independent buckets, such as separate carts or parallel conversations, `SessionId` makes the handle an explicit string argument that the agent obtains from `create_session` and supplies on each call. See [Session State](/servers/sessions). ## Background tasks -Long-running work runs as a background task: the server accepts the call, returns a handle, and the client polls for the result while the work proceeds. Tasks left the core MCP spec during the SDK v2 rebuild and returned as the `io.modelcontextprotocol/tasks` extension (SEP-2663), which FastMCP implements end to end in the optional `fastmcp-tasks` package. The durable execution engine that made FastMCP 3's tasks reliable — [Docket](https://github.com/chrisguidry/docket) — carries straight over, and `@mcp.tool(task=True)` remains the authoring surface, so the wire protocol modernizing underneath costs you no code change. See [Background Tasks](/servers/tasks). +Long-running work runs as a background task: the server accepts the call and returns a handle immediately, and the client polls for the result while the work proceeds. Tasks left the core MCP spec during the SDK rewrite and returned as the `io.modelcontextprotocol/tasks` extension, which FastMCP implements end to end in the optional `fastmcp-tasks` package. + +`@mcp.tool(task=True)` remains the authoring surface and [Docket](https://github.com/chrisguidry/docket) still provides the durable execution engine, so the wire protocol modernizing underneath costs you no code change. What's new is the registration: tasks arrive as an extension you add to the server. + +```python +import asyncio +from fastmcp import FastMCP +from fastmcp_tasks import TasksExtension + +mcp = FastMCP("MyServer") +mcp.add_extension(TasksExtension()) + + +@mcp.tool(task=True) +async def slow_computation(duration: int) -> str: + """A long-running operation.""" + await asyncio.sleep(duration) + return f"Completed in {duration} seconds" +``` + +A FastMCP client handles the handle-and-poll cycle transparently, so `client.call_tool(...)` looks the same whether or not the call ran in the background. See [Background Tasks](/servers/tasks). ## Server extensions -Background tasks are the first capability built on a more general one: FastMCP 4 makes MCP extensions — capability-negotiated protocol features named by a reverse-DNS string (SEP-2133) — a first-class surface. `FastMCP.add_extension()` lets an extension advertise a capability, add request methods, intercept `tools/call`, and run a lifespan hook, all with full access to the component registry, `Context`, and auth. The same extensions flow through the client with `Client(extensions=...)`. A cross-cutting protocol feature stops being surgery on core and becomes a supported plugin. +Background tasks are the first capability built on a more general one. An MCP extension is a protocol feature named by a reverse-DNS string and negotiated as a capability, and FastMCP 4 makes extensions a first-class surface rather than something only the framework can add. + +`FastMCP.add_extension()` lets an extension advertise a capability, add request methods, intercept `tools/call`, and run a lifespan hook, all with full access to the component registry, `Context`, and auth. The same extensions flow through the client with `Client(extensions=...)`. A cross-cutting protocol feature becomes a supported plugin instead of surgery on core, and `TasksExtension` is the worked example of everything the interface allows. See [Server Extensions](/servers/extensions). ## Argument completion -When a client offers autocomplete for a prompt argument or a resource-template parameter, it asks the server which values fit — narrowing the list as the user types. FastMCP 4 lets a server answer. A single `@mcp.completion` handler receives the reference being completed, the argument and its partial value, and the arguments the user has already supplied, and returns the candidates the client surfaces as suggestions. Because the handler sees the earlier arguments, completions can depend on them — a `repo` parameter suggesting only repositories under the `owner` already chosen. +When a client offers autocomplete for a prompt argument or a resource-template parameter, it asks the server which values fit, narrowing the list as the user types. FastMCP 4 lets a server answer. A single `@mcp.completion` handler receives the reference being completed, the argument and its partial value, and the arguments the user has already supplied, and returns the candidates the client surfaces as suggestions. + +Because the handler sees the earlier arguments, completions can depend on them: a `repo` parameter can suggest only the repositories under the `owner` already chosen. ```python from fastmcp import FastMCP @@ -67,11 +130,11 @@ def complete(ref, argument, context): return None ``` -Registering a handler advertises the completions capability during negotiation, so a client only sends requests to a server that answers them — the same on both protocol eras. See [Argument Completion](/servers/completions). +Registering a handler advertises the completions capability during negotiation, so a client only sends requests to a server that answers them, identically on both protocol eras. See [Argument Completion](/servers/completions). ## Enterprise identity -FastMCP 4 ships a complete server-side implementation of identity assertion (SEP-990): enterprise "on-behalf-of" access, where a corporate identity provider issues a signed assertion, the user's agent presents it, and the server mints a short-lived token — no browser login and no per-user consent screen. Behind one parameter on the existing auth providers, FastMCP performs the full signature verification, binding checks, replay rejection, and scoped token issuance. +FastMCP 4 ships a complete server-side implementation of identity assertion, the enterprise "on-behalf-of" flow: a corporate identity provider issues a signed assertion, the user's agent presents it, and the server mints a short-lived token, with no browser login and no per-user consent screen. Behind one parameter on the existing auth providers, FastMCP performs the signature verification, binding checks, replay rejection, and scoped token issuance. ```python from fastmcp import FastMCP @@ -86,7 +149,7 @@ mcp = FastMCP("Internal API", auth=auth) The asserted subject flows into the normal auth context, so tools read it through `get_access_token()` like any other identity. See [Identity Assertion](/servers/auth/oauth-proxy#identity-assertion-sep-990). -Authorizing a caller by role is a related, provider-agnostic need. Scopes are standardized, so `require_scopes` behaves the same everywhere, but roles and groups are not part of OIDC and every provider files them under a different claim. `require_roles` handles the comparison and takes an `extract` callable naming where to look, so Keycloak's `realm_access.roles`, Cognito's `cognito:groups`, and Auth0's per-tenant namespaced claims all work without FastMCP guessing. +Authorizing a caller by role is a related, provider-agnostic need. Scopes are standardized, so `require_scopes` behaves the same everywhere, but roles and groups are not part of OIDC and every provider files them under a different claim. `require_roles` handles the comparison and takes an `extract` callable naming where to look, so Keycloak's `realm_access.roles`, Cognito's `cognito:groups`, and Auth0's namespaced claims all work without FastMCP guessing. ```python from fastmcp import FastMCP @@ -94,15 +157,16 @@ from fastmcp.server.auth import require_roles mcp = FastMCP("Internal API") + @mcp.tool(auth=require_roles("admin", extract=lambda c: c["realm_access"]["roles"])) def rotate_credentials() -> str: """Only callable by a caller holding the 'admin' role.""" return "Rotated" ``` -This illustrates the check in isolation — enforcing it for real needs an HTTP-transport server with a token-validating `auth` provider configured (a `JWTVerifier`, a `RemoteAuthProvider`, or a provider built on one, such as `KeycloakAuthProvider`, all expose claims directly), since STDIO has no OAuth concept and skips every check. See [Authorization](/servers/authorization#require_roles) for the full picture. +That example shows the check in isolation. Enforcing it for real needs an HTTP-transport server with a token-validating `auth` provider configured, since STDIO has no OAuth concept and skips every check. A `JWTVerifier`, a `RemoteAuthProvider`, or any provider built on one such as `KeycloakAuthProvider` all expose claims directly. See [Authorization](/servers/authorization#require_roles). -The client side of enterprise auth arrived too. Not every FastMCP client has a user behind it — a backend service, a scheduled job, one MCP server calling another — and `ClientCredentialsOAuthProvider` authenticates one of those to a protected server with the OAuth 2.0 client-credentials grant: no browser, no redirect, no consent screen. +The client side arrived too. Plenty of FastMCP clients have no user behind them, such as a backend service, a scheduled job, or one MCP server calling another. `ClientCredentialsOAuthProvider` authenticates one of those to a protected server with the OAuth 2.0 client-credentials grant: no browser, no redirect, no consent screen. ```python import asyncio @@ -127,9 +191,9 @@ asyncio.run(main()) See [Machine-to-Machine Authentication](/clients/auth/client-credentials). -## Faster and safer +## Response caching -Two more capabilities arrive by default. Response caching (SEP-2549) lets a server stamp freshness hints on its results that a caching [client](/clients/client#response-caching) reuses without a round trip, and a distributed `KeyValueResponseCacheStore` backs that cache with Redis or any key-value store, so a fleet of clients or proxy replicas shares fills. +A server can stamp freshness hints on its results, and a caching client reuses a result within that window instead of making the round trip. Set the defaults on the server and every response carries them. ```python from fastmcp import FastMCP @@ -137,10 +201,10 @@ from fastmcp import FastMCP mcp = FastMCP("Weather", cache_ttl=300, cache_scope="public") ``` -Security tightened in the same release: every templated resource screens its parameters for path traversal, absolute paths, and null bytes before the handler runs — [path security](/servers/resources#path-security) on by default, covering mounted and proxied templates too. +Backing the client's cache with the distributed `KeyValueResponseCacheStore` puts it in Redis or any key-value store, so a fleet of clients or proxy replicas shares fills rather than each paying for its own. See [Response caching](/clients/client#response-caching). -The OAuth flow got more precise as well. Dynamic Client Registration now honors a client's declared `application_type` (SEP-837): the permissive loopback and app-scheme callbacks MCP clients rely on stay the default for `"native"`, while a client that registers as `"web"` is held to stricter browser-app redirect rules. And when `AuthMiddleware` denies a call specifically for a missing scope, it raises `InsufficientScopeError` naming exactly which scopes would fix it (SEP-2350), so a caller re-authorizes precisely instead of retrying blind. See [Application Type](/servers/auth/oauth-proxy#application-type-web-vs-native) and [Signaling Scope Shortfalls](/servers/authorization#signaling-scope-shortfalls). +## Security defaults -A gateway or load balancer in front of your server can now route a request without parsing its JSON-RPC body: on a modern connection, FastMCP's client attaches the method, target name, and opted-in argument values as HTTP headers (SEP-2243), so an intermediary dispatches on headers alone. See [Gateway Routing Headers](/deployment/http#gateway-routing-headers). +Templated resources now screen their parameters for path traversal, absolute paths, and null bytes before the handler runs. This is on by default and covers mounted and proxied templates too, so a template that interpolates a parameter into a filesystem path no longer has to validate it by hand. See [path security](/servers/resources#path-security). -When you're ready to move a server to v4, [Upgrading from FastMCP 3](/getting-started/upgrading/from-fastmcp-3) walks through every change and what it looks like in practice. +The OAuth flow got more precise in two places. Dynamic Client Registration honors a client's declared `application_type`: the permissive loopback and app-scheme callbacks that MCP clients rely on stay the default for `"native"`, while a client registering as `"web"` is held to stricter browser-app redirect rules. And when `AuthMiddleware` denies a call specifically for a missing scope, it raises `InsufficientScopeError` naming which scopes would fix it, so a caller re-authorizes precisely instead of retrying blind. See [Application Type](/servers/auth/oauth-proxy#application-type-web-vs-native) and [Signaling Scope Shortfalls](/servers/authorization#signaling-scope-shortfalls). diff --git a/docs/more/faq.mdx b/docs/more/faq.mdx index 29b0f7e89..1ac3c2c7d 100644 --- a/docs/more/faq.mdx +++ b/docs/more/faq.mdx @@ -58,7 +58,7 @@ Take the paths you need as ordinary tool arguments. The agent already knows whic Because logging is a *notification* and sampling was a *request*. A notification is fire-and-forget: your server emits it down the response stream the caller already opened, and nothing has to be held open on the server's behalf. A request needs an answer to come back the other way, which requires a live connection the server can reach into. -The modern protocol kept every server notification — `notifications/message`, `notifications/progress`, and the list-changed family — and removed the server-to-client request direction entirely. So `ctx.info()`, `ctx.debug()`, and `ctx.report_progress()` reach the client mid-call on every era, while sampling and roots have no era-agnostic form and were dropped. [Sampling](/servers/sampling#requests-and-notifications) works through the distinction in full. +The modern protocol kept every server notification — `notifications/message`, `notifications/progress`, and the list-changed family — and removed the server-to-client request direction entirely. So `ctx.info()`, `ctx.debug()`, and `ctx.report_progress()` reach the client mid-call on every era, while sampling and roots have no era-agnostic form and were dropped. [Sampling](/servers/sampling#the-removed-methods) works through the distinction in full. You may see an `MCPDeprecationWarning` from the SDK about the logging capability being deprecated as of `2026-07-28`. It refers to the capability declaration, not to the notification, and delivery is unaffected. diff --git a/docs/servers/extensions.mdx b/docs/servers/extensions.mdx new file mode 100644 index 000000000..de81d2251 --- /dev/null +++ b/docs/servers/extensions.mdx @@ -0,0 +1,166 @@ +--- +title: Server Extensions +sidebarTitle: Extensions +description: Add negotiated protocol features to a server without forking the framework. +icon: plug +--- + +import { VersionBadge } from "/snippets/version-badge.mdx" + + + +An MCP extension is a protocol feature that lives outside the core spec, named by a reverse-DNS identifier and negotiated as a capability. A server advertises the extensions it implements, and a client advertises the ones it understands. That negotiation is per request: a client repeats its extension capabilities in every request's `_meta`, so a handler can always tell whether the caller opted in to this particular call. + +Honoring that opt-in is the extension's job, not the framework's. FastMCP advertises your capability and routes your methods, but it does not filter callers for you, so an extension that changes behavior must check before it acts. The [tool-call interceptor](#intercepting-tool-calls) below shows the check. + +FastMCP 4 makes extensions a first-class surface. `FastMCP.add_extension()` takes an object that can advertise a capability, serve new request methods, wrap every `tools/call`, and own resources for the life of the server. [Background tasks](/servers/tasks) are built this way, on the same public interface available to you, so a cross-cutting protocol feature becomes a plugin rather than a change to FastMCP itself. + +## Writing an extension + +Subclass `ServerExtension` and set an `identifier`. The identifier must carry a reverse-DNS prefix in `vendor-prefix/name` form, which FastMCP validates when the class is defined, so a malformed one fails immediately rather than at connection time. Everything else is optional: each contribution method has a working default, and a useful extension often overrides just one. + +Registering the extension binds it to the server and advertises its capability. The capability is advertised only while the extension is registered, and registering two extensions with the same identifier is an error. + +```python +from fastmcp import FastMCP +from fastmcp.server.extensions import ServerExtension + + +class CallCounterExtension(ServerExtension): + identifier = "com.example/call-counter" + + def __init__(self) -> None: + self.count = 0 + + +mcp = FastMCP("Demo") +mcp.add_extension(CallCounterExtension()) +``` + +Register extensions before the server starts. Adding one after the lifespan is running raises, because the extension's own lifespan could no longer run and it would end up silently half-active. + +An extension reaches the rest of the server through `self.server`, which is the `FastMCP` instance it was registered on. That is how handlers and interceptors get at the component registry, the request [`Context`](/servers/context), and the authenticated caller. + +## Advertising settings + +Some extensions need to tell the client how they are configured: a size limit, a supported mode, a flag. Override `settings()` to return a JSON-serializable dict, and it appears on the wire under `capabilities.extensions[identifier]`. The default is an empty dict, which advertises the extension with no settings attached. + +```python +from typing import Any + +from fastmcp import FastMCP +from fastmcp.server.extensions import ServerExtension + + +class UploadExtension(ServerExtension): + identifier = "com.example/uploads" + + def settings(self) -> dict[str, Any]: + return {"maxBytes": 10_000_000, "resumable": True} + + +mcp = FastMCP("Demo") +mcp.add_extension(UploadExtension()) +``` + +A client reads these alongside the capability itself, so it can adapt before making a single call. + +## Adding request methods + +An extension can serve request methods the core spec does not define. Return a `MethodBinding` from `methods()` naming the wire method, the Pydantic model its params validate against, and the handler to run. + +Extension methods are strictly additive. Binding a spec-defined method like `tools/call` raises at construction, because doing so would silently shadow the server's own handler. To change how a core method behaves, use [middleware](/servers/middleware) or the tool-call interceptor below. + +The params model should subclass `RequestParams` so `_meta` parses uniformly, and the handler receives the request context and the validated params. + +```python +from typing import Any + +from mcp.types import RequestParams +from fastmcp.server.extensions import MethodBinding, ServerExtension + + +class GetCallCountParams(RequestParams): + pass + + +class CallCounterExtension(ServerExtension): + identifier = "com.example/call-counter" + + def __init__(self) -> None: + self.count = 0 + + def methods(self) -> list[MethodBinding]: + return [ + MethodBinding( + method="callCounter/get", + params_type=GetCallCountParams, + handler=self.get_count, + ) + ] + + async def get_count(self, ctx, params: GetCallCountParams) -> dict[str, Any]: + return {"count": self.count} +``` + +Setting `protocol_versions` on a binding restricts the method to specific wire versions, and a request at any other version is rejected as `METHOD_NOT_FOUND`. Leaving it unset, the default, serves the method on every version. + +## Intercepting tool calls + +Override `intercept_tool_call()` to wrap every `tools/call` the server handles. The interceptor runs after the FastMCP middleware chain and immediately before the tool body, making it the last gate before execution. Await `call_next()` to let the call proceed, or return a result without awaiting it to short-circuit. + +Every registered interceptor runs on every tool call, including calls from clients that never advertised your extension. FastMCP does not gate this for you, so an interceptor that changes what the caller gets back must first confirm the caller opted in. `context.client_extension_settings(identifier)` returns the settings the client declared for this request, or `None` when it declared nothing. + +```python +from fastmcp import FastMCP +from fastmcp.server.extensions import ServerExtension + + +class CallCounterExtension(ServerExtension): + identifier = "com.example/call-counter" + + def __init__(self) -> None: + self.count = 0 + + async def intercept_tool_call(self, params, context, call_next): + if context.client_extension_settings(self.identifier) is None: + return await call_next() + self.count += 1 + return await call_next() + + +mcp = FastMCP("Demo") +mcp.add_extension(CallCounterExtension()) +``` + +Counting is harmless either way, so this example passes unaware callers straight through. The check becomes essential the moment an interceptor short-circuits: returning an extension-specific result to a client that never negotiated the extension hands it a shape it has no way to understand. Request methods have the same requirement, and `self.client_settings(ctx)` is the equivalent inside a handler. + +`params` holds the validated `tools/call` params, and `context` is the FastMCP `Context`, so the tool being invoked is reachable as `context.fastmcp.get_tool(params.name)` along with auth scope and the server itself. When several extensions intercept, they nest with the first-registered outermost. + +Reach for middleware when you want to observe or modify requests generally; reach for an interceptor when the behavior belongs to a negotiated capability and should exist only while that extension is registered. + +## Owning resources + +An extension that owns something with a lifecycle, such as a connection pool or a background worker, overrides `lifespan()` to return an async context manager. FastMCP enters it with the server's own [lifespan](/servers/lifespan) and exits it on shutdown, so setup and teardown stay with the extension that needs them rather than leaking into the application's startup code. + +The lifespan is entered once per runtime tree, at the root. This matters when you compose servers: extensions are served by the server they are registered on, and a mounted child's extensions do not propagate upward. The root server owns the wire, so only root-registered extensions advertise capabilities and answer methods. Register extensions on the server you actually run. + +## Client extensions + +The client half of an extension is what makes negotiation two-sided. Pass `ClientExtension` instances to `Client(extensions=...)` and each contributes its capability advertisement, its result claims, and its notification bindings to the underlying session. A claimed `call_tool` result is then resolved transparently through the extension that owns it. + +When a client needs only to say it understands an extension, without implementing behavior for it, `advertise()` produces an advertise-only entry. + +```python +from fastmcp import Client +from mcp.client import advertise + +client = Client( + "https://example.com/mcp", + extensions=[advertise("com.example/uploads", {"maxBytes": 10_000_000})], +) +``` + +Advertise only what you genuinely support: the advertisement asserts wire compatibility, and claiming an extension you have not implemented invites the server to use a feature you cannot answer. For anything behavioral, construct the real extension instead. + +Claimed result shapes are a modern-protocol feature and stay inert on a legacy connection, so an extension-aware client is still safe to point at an older server. diff --git a/docs/servers/providers/overview.mdx b/docs/servers/providers/overview.mdx index c19073f78..2f23f76a5 100644 --- a/docs/servers/providers/overview.mdx +++ b/docs/servers/providers/overview.mdx @@ -70,12 +70,6 @@ When a client requests a component by name or URI, FastMCP queries providers and - [Proxy a remote server](/servers/providers/proxy) through yours - [Control visibility state](/servers/visibility) of components - [Build dynamic sources](/servers/providers/custom) like database-backed tools +- [Transform components](/servers/transforms/transforms) to namespace, rename, or modify them -## Next Steps - -- [Local](/servers/providers/local) - How decorators work -- [Mounting](/servers/composition) - Compose servers together -- [Proxying](/servers/providers/proxy) - Connect to remote servers -- [Transforms](/servers/transforms/transforms) - Namespace, rename, and modify components -- [Visibility](/servers/visibility) - Control which components clients can access -- [Custom](/servers/providers/custom) - Build your own providers +The decorators you already use are themselves a provider: [`LocalProvider`](/servers/providers/local) is what backs `@mcp.tool` and its siblings. diff --git a/docs/servers/storage-backends.mdx b/docs/servers/storage-backends.mdx index 30bdf7b93..32e6530eb 100644 --- a/docs/servers/storage-backends.mdx +++ b/docs/servers/storage-backends.mdx @@ -199,7 +199,7 @@ Both parameters are required for production. **Wrap your storage in `FernetEncry ### Response Caching Middleware -The [Response Caching Middleware](/servers/middleware#caching-middleware) caches tool calls, resource reads, and prompt requests. Storage configuration is passed via the `cache_storage` parameter: +The [Response Caching Middleware](/servers/middleware#caching) caches tool calls, resource reads, and prompt requests. Storage configuration is passed via the `cache_storage` parameter: ```python from pathlib import Path @@ -289,6 +289,6 @@ This allows clients to reconnect without re-authenticating after restarts. ## More Resources - [py-key-value-aio GitHub](https://github.com/strawgate/py-key-value) - Full library documentation -- [Response Caching Middleware](/servers/middleware#caching-middleware) - Using storage for caching +- [Response Caching Middleware](/servers/middleware#caching) - Using storage for caching - [OAuth Token Security](/deployment/http#oauth-token-security) - Production OAuth configuration - [HTTP Deployment](/deployment/http) - Complete deployment guide diff --git a/docs/servers/tools.mdx b/docs/servers/tools.mdx index f7fe31ec0..e87ebedd4 100644 --- a/docs/servers/tools.mdx +++ b/docs/servers/tools.mdx @@ -431,7 +431,7 @@ def get_user_details(user_id: str = Depends(get_user_id)) -> str: return f"Details for {user_id}" ``` -See [Custom Dependencies](/servers/context#custom-dependencies) for more details on dependency injection. +See [Custom Dependencies](/servers/dependency-injection#custom-dependencies) for more details on dependency injection. ## Return Values diff --git a/docs/servers/transforms/code-mode.mdx b/docs/servers/transforms/code-mode.mdx index 5b04fcab5..0e7e4f50d 100644 --- a/docs/servers/transforms/code-mode.mdx +++ b/docs/servers/transforms/code-mode.mdx @@ -140,7 +140,7 @@ You can cap result count with `default_limit`. The LLM can also override the lim Search(default_limit=5) # return at most 5 results per search ``` -If your tools use [tags](/servers/tools#tags), Search also accepts a `tags` parameter so the LLM can narrow results to specific categories before searching. +If your tools use [tags](/servers/visibility#tags), Search also accepts a `tags` parameter so the LLM can narrow results to specific categories before searching. ### GetSchemas @@ -148,7 +148,7 @@ If your tools use [tags](/servers/tools#tags), Search also accepts a `tags` para ### GetTags -`GetTags` lets the LLM browse tools by category using [tag](/servers/tools#tags) metadata. At brief detail, the LLM sees tag names with counts. At full detail, it sees tools listed under each tag: +`GetTags` lets the LLM browse tools by category using [tag](/servers/visibility#tags) metadata. At brief detail, the LLM sees tag names with counts. At full detail, it sees tools listed under each tag: ``` - math (3 tools) @@ -187,7 +187,7 @@ from fastmcp.experimental.transforms.code_mode import CodeMode mcp = FastMCP("Server", transforms=[CodeMode()]) ``` -If your tools use [tags](/servers/tools#tags), add `GetTags` so the LLM can browse by category before searching — giving it four stages of progressive disclosure: +If your tools use [tags](/servers/visibility#tags), add `GetTags` so the LLM can browse by category before searching — giving it four stages of progressive disclosure: ```python from fastmcp import FastMCP From 0792ac812c3240a8256d44fbbc01caa97e4cb8cc Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Wed, 29 Jul 2026 09:51:13 -0400 Subject: [PATCH 19/53] Improve the v4 docs (#4707) --- docs/apps/providers/file-upload.mdx | 25 ++- docs/changelog.mdx | 2 +- docs/cli/overview.mdx | 4 +- docs/clients/client.mdx | 8 +- docs/clients/logging.mdx | 11 +- docs/clients/resources.mdx | 13 +- docs/deployment/server-configuration.mdx | 15 +- docs/docs.json | 2 +- docs/getting-started/installation.mdx | 4 +- docs/getting-started/quickstart.mdx | 6 +- docs/getting-started/whats-new.mdx | 200 ++++++++++-------- docs/integrations/anthropic.mdx | 22 +- docs/integrations/github.mdx | 4 +- docs/integrations/mcp-json-configuration.mdx | 4 +- docs/integrations/permit.mdx | 6 +- docs/servers/auth/oauth-proxy.mdx | 7 +- docs/servers/auth/oidc-proxy.mdx | 2 +- docs/servers/auth/remote-oauth.mdx | 10 +- docs/servers/middleware.mdx | 4 +- docs/servers/prompts.mdx | 4 +- docs/servers/resources.mdx | 6 +- docs/servers/tools.mdx | 8 +- docs/tutorials/mcp.mdx | 10 +- docs/updates.mdx | 6 +- .../fastmcp/server/auth/oauth_proxy/proxy.py | 2 +- 25 files changed, 220 insertions(+), 165 deletions(-) diff --git a/docs/apps/providers/file-upload.mdx b/docs/apps/providers/file-upload.mdx index b9709d946..f10ef0da7 100644 --- a/docs/apps/providers/file-upload.mdx +++ b/docs/apps/providers/file-upload.mdx @@ -59,16 +59,24 @@ This works with **stdio**, **SSE**, and **stateful HTTP** transports, where sess In **stateless HTTP** mode, each request creates a new session object with a new ID. Files stored during one request (e.g. the UI upload) will be invisible to the next request (e.g. the LLM calling `list_files`). You **must** override `_get_scope_key` to use a stable identifier like a user ID from your auth token. -For stateless deployments, override `_get_scope_key` to return a stable identifier. For example, to scope files by authenticated user: +For stateless deployments, override `_get_scope_key` to return a stable identifier. To scope files by authenticated user, read the caller from `get_access_token()`. + +Reject the request when there is no subject to key on. `get_access_token()` returns `None` on an unauthenticated request, and `subject` is optional even on a valid token, since not every verifier populates it. Returning a fallback in either case would put every such caller in one shared bucket, so they would see each other's uploads. ```python from fastmcp.apps.file_upload import FileUpload +from fastmcp.server.dependencies import get_access_token class UserScopedUpload(FileUpload): def _get_scope_key(self, ctx): - return ctx.access_token["sub"] + token = get_access_token() + if token is None or not token.subject: + raise ValueError("File scoping requires an authenticated user with a subject") + return token.subject ``` +If your provider carries the user identity in a different claim, read it from `token.claims` and validate it the same way. + For process-wide shared storage (all users see all files): ```python @@ -85,10 +93,17 @@ The default implementation stores files in memory for the lifetime of the server import base64 from fastmcp.apps.file_upload import FileUpload +from fastmcp.server.dependencies import get_access_token class S3Upload(FileUpload): + def _get_scope_key(self, ctx): + token = get_access_token() + if token is None or not token.subject: + raise ValueError("File scoping requires an authenticated user with a subject") + return token.subject + def on_store(self, files, ctx): - user_id = ctx.access_token["sub"] + user_id = self._get_scope_key(ctx) for f in files: s3.put_object( Bucket="uploads", @@ -98,7 +113,7 @@ class S3Upload(FileUpload): return self.on_list(ctx) def on_list(self, ctx): - user_id = ctx.access_token["sub"] + user_id = self._get_scope_key(ctx) objects = s3.list_objects(Bucket="uploads", Prefix=f"{user_id}/") return [ { @@ -112,7 +127,7 @@ class S3Upload(FileUpload): ] def on_read(self, name, ctx): - user_id = ctx.access_token["sub"] + user_id = self._get_scope_key(ctx) obj = s3.get_object(Bucket="uploads", Key=f"{user_id}/{name}") content = obj["Body"].read() return { diff --git a/docs/changelog.mdx b/docs/changelog.mdx index 09a52bbd8..de17a3f8e 100644 --- a/docs/changelog.mdx +++ b/docs/changelog.mdx @@ -9,7 +9,7 @@ tag: NEW **[v4.0.0b1: Fourgone Conclusion](https://github.com/PrefectHQ/fastmcp/releases/tag/v4.0.0b1)** -FastMCP 4 rebuilds the framework on the MCP Python SDK v2, and this beta is the first release to run on the SDK's stable 2.0. The SDK v2 rewrote the protocol layer end to end — protocol types moved into a standalone `mcp_types` package, every model field renamed from camelCase to snake_case in Python, and the server's request-handling model replaced — and FastMCP absorbs nearly all of it, so most FastMCP 3 servers run untouched. On that foundation v4 serves the sessionless `2026-07-28` protocol and the older handshake from one server, adds stateless session state and background tasks, makes protocol extensions a first-class surface, and removes server-initiated sampling and roots from the server API. +FastMCP 4 makes stateful MCP applications work on the sessionless `2026-07-28` protocol while one deployment continues serving handshake-era clients. Tools can ask follow-up questions across requests, preserve authenticated user state, and move long-running work into background tasks without sticky sessions. Protocol extensions and enterprise identity become first-class surfaces, and most FastMCP 3 servers upgrade unchanged even though MCP Python SDK v2 rewrote the engine underneath them. Server-initiated sampling and roots are removed from the server API; the [upgrade guide](/getting-started/upgrading/from-fastmcp-3) covers their replacements. ### New Features 🎉 * Migrate to MCP Python SDK v2 by [@jlowin](https://github.com/jlowin) in [#4437](https://github.com/PrefectHQ/fastmcp/pull/4437) diff --git a/docs/cli/overview.mdx b/docs/cli/overview.mdx index c002c0c1e..9085daaa8 100644 --- a/docs/cli/overview.mdx +++ b/docs/cli/overview.mdx @@ -89,10 +89,10 @@ To skip authentication entirely — useful for local development servers — pas fastmcp call http://localhost:8000/mcp my_tool --auth none ``` -You can also pass a bearer token directly: +You can also pass a bearer token directly. Give the token value on its own; FastMCP adds the `Bearer` prefix when it builds the `Authorization` header. ```bash -fastmcp list http://localhost:8000/mcp --auth "Bearer sk-..." +fastmcp list http://localhost:8000/mcp --auth "sk-..." ``` ## Transport Override diff --git a/docs/clients/client.mdx b/docs/clients/client.mdx index 092738e7c..fb910777f 100644 --- a/docs/clients/client.mdx +++ b/docs/clients/client.mdx @@ -144,12 +144,12 @@ async with Client(mcp) as client: print(f"Capabilities: {client.server_capabilities.tools}") ``` -For advanced scenarios where you need precise control over when initialization happens, disable automatic initialization and call `initialize()` manually: +For advanced scenarios where you need precise control over when initialization happens, disable automatic initialization and call `initialize()` manually. `initialize()` is a handshake-era operation, so pin the connection with `mode="legacy"`: the modern protocol has no `initialize` round trip, and calling it on a modern connection raises. ```python from fastmcp import Client -client = Client("my_mcp_server.py", auto_initialize=False) +client = Client("my_mcp_server.py", auto_initialize=False, mode="legacy") async with client: # Connection established, but not initialized yet @@ -219,7 +219,7 @@ The SSE transport is legacy-only — it cannot carry the sessionless modern era -The client can cache the results of `list_tools`, `list_resources`, `list_prompts`, and `read_resource` so that repeated calls avoid a network round-trip. Caching is opt-in and honors the server's own cache hints, so it only takes effect against modern-era servers that advertise them — a cache is inert on a legacy connection. +The client can cache the results of `list_tools`, `list_resources`, and `list_prompts` so that repeated calls avoid a network round-trip. Caching is opt-in and honors the server's own cache hints, so it only takes effect against modern-era servers that advertise them — a cache is inert on a legacy connection. Enable the default in-memory cache by passing `cache=True`. It respects the `ttlMs` and `cacheScope` hints the server attaches to each response. @@ -243,7 +243,7 @@ config = CacheConfig(target_id="weather-api", default_ttl_ms=60_000) client = Client("https://example.com/mcp", mode="auto", cache=config) ``` -The high-level `list_tools`, `list_resources`, `list_prompts`, and `read_resource` methods always use the cache when one is configured. To override the behavior for a single call, use the lower-level `*_mcp` variants, which accept a `cache_mode` argument: `"use"` (the default) serves and stores, `"refresh"` stores a fresh result without serving a cached one, and `"bypass"` skips the cache entirely. +The high-level `list_tools`, `list_resources`, and `list_prompts` methods always use the cache when one is configured. To override the behavior for a single call, use the lower-level `list_tools_mcp`, `list_resources_mcp`, `list_resource_templates_mcp`, and `list_prompts_mcp` variants, which accept a `cache_mode` argument: `"use"` (the default) serves and stores, `"refresh"` stores a fresh result without serving a cached one, and `"bypass"` skips the cache entirely. ```python async with client: diff --git a/docs/clients/logging.mdx b/docs/clients/logging.mdx index 0fb9f735b..407b1ebd5 100644 --- a/docs/clients/logging.mdx +++ b/docs/clients/logging.mdx @@ -28,7 +28,16 @@ logging.basicConfig( ) logger = logging.getLogger(__name__) -LOGGING_LEVEL_MAP = logging.getLevelNamesMapping() +LOGGING_LEVEL_MAP = { + "DEBUG": logging.DEBUG, + "INFO": logging.INFO, + "NOTICE": logging.INFO, + "WARNING": logging.WARNING, + "ERROR": logging.ERROR, + "CRITICAL": logging.CRITICAL, + "ALERT": logging.CRITICAL, + "EMERGENCY": logging.CRITICAL, +} async def log_handler(message: LogMessage): """Forward MCP server logs to Python's logging system.""" diff --git a/docs/clients/resources.mdx b/docs/clients/resources.mdx index 68aaaf18b..041ad0978 100644 --- a/docs/clients/resources.mdx +++ b/docs/clients/resources.mdx @@ -58,18 +58,25 @@ async with client: Binary resources include images, PDFs, and other non-text data: +Binary resources arrive as `BlobResourceContents`, whose `blob` field is a base64 **string**, so decode it before writing bytes to disk: + ```python +import base64 + +from mcp_types import BlobResourceContents + async with client: content = await client.read_resource("resource://images/logo.png") for item in content: - if hasattr(item, 'blob'): - print(f"Binary content: {len(item.blob)} bytes") + if isinstance(item, BlobResourceContents): + data = base64.b64decode(item.blob) + print(f"Binary content: {len(data)} bytes") print(f"MIME type: {item.mime_type}") # Save to file with open("downloaded_logo.png", "wb") as f: - f.write(item.blob) + f.write(data) ``` ## Multi-Server Clients diff --git a/docs/deployment/server-configuration.mdx b/docs/deployment/server-configuration.mdx index 4eb966231..c67d5ef1f 100644 --- a/docs/deployment/server-configuration.mdx +++ b/docs/deployment/server-configuration.mdx @@ -39,30 +39,33 @@ The `fastmcp.json` configuration answers three fundamental questions about your This conceptual model helps you understand the purpose of each configuration section and organize your settings effectively. The configuration file maps directly to these three concerns: +`source` is the *where*, `environment` the *what*, and `deployment` the *how*: + ```json { "$schema": "https://gofastmcp.com/public/schemas/fastmcp.json/v1.json", "source": { - // WHERE: Location of your server code - "type": "filesystem", // Optional, defaults to "filesystem" + "type": "filesystem", "path": "server.py", "entrypoint": "mcp" }, "environment": { - // WHAT: Environment setup and dependencies - "type": "uv", // Optional, defaults to "uv" + "type": "uv", "python": ">=3.10", "dependencies": ["pandas", "numpy"] }, "deployment": { - // HOW: Runtime configuration "transport": "stdio", "log_level": "INFO" } } ``` -Only the `source` field is required. The `environment` and `deployment` sections are optional and provide additional configuration when needed. +Only the `source` field is required. The `environment` and `deployment` sections are optional and provide additional configuration when needed. Both `type` fields shown above are optional too, defaulting to `"filesystem"` and `"uv"` respectively. + + +`fastmcp.json` is parsed as strict JSON, so it accepts no comments or trailing commas. + ### JSON Schema Support diff --git a/docs/docs.json b/docs/docs.json index 22bc3c4b9..a3490a88a 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -16,7 +16,7 @@ "dark": "#475569", "light": "#1e3a5f" }, - "content": "FastMCP 4 is in beta — check out [what's new](/getting-started/whats-new)!" + "content": "FastMCP 4 is in beta — build stateful applications on sessionless MCP. [See what's new](/getting-started/whats-new)." }, "colors": { "dark": "#f72585", diff --git a/docs/getting-started/installation.mdx b/docs/getting-started/installation.mdx index c3c1bdacd..8c3167fb6 100644 --- a/docs/getting-started/installation.mdx +++ b/docs/getting-started/installation.mdx @@ -115,8 +115,8 @@ FastMCP follows semantic versioning with pragmatic adaptations for the rapidly e For production use, always pin to exact versions: ``` -fastmcp==4.0.0 # Good -fastmcp>=4.0.0 # Bad - may install breaking changes +fastmcp==4.0.0b1 # Good - an exact version +fastmcp>=4.0.0 # Bad - may install breaking changes ``` See the full [versioning and release policy](/development/releases#versioning-policy) for details on our public API, deprecation practices, and breaking change philosophy. diff --git a/docs/getting-started/quickstart.mdx b/docs/getting-started/quickstart.mdx index ae157d906..79c4599a3 100644 --- a/docs/getting-started/quickstart.mdx +++ b/docs/getting-started/quickstart.mdx @@ -142,9 +142,11 @@ def greet(name: str) -> PrefabApp: You can preview app tools locally with `fastmcp dev apps my_server.py` — no MCP host required. See the [Apps overview](/apps/overview) for the full guide, including state management, forms, charts, and server-connected interactivity. -## Deploy to Prefect Horizon +## Deploy Your Server -[Prefect Horizon](https://horizon.prefect.io?utm_source=gofastmcp&utm_medium=docs) is the enterprise MCP platform built by the FastMCP team at [Prefect](https://www.prefect.io). It provides managed hosting, authentication, access control, and observability for MCP servers. +FastMCP HTTP servers run anywhere you can host a Python application. The [HTTP deployment guide](/deployment/http) covers the transport settings and security boundaries for self-managed infrastructure. + +For a managed deployment, [Prefect Horizon](https://horizon.prefect.io?utm_source=gofastmcp&utm_medium=docs) is the enterprise MCP platform built by the FastMCP team at [Prefect](https://www.prefect.io). It provides hosting, authentication, access control, and observability for MCP servers. Horizon is **free for personal projects** and offers enterprise governance for teams. diff --git a/docs/getting-started/whats-new.mdx b/docs/getting-started/whats-new.mdx index 230bb5844..cebc3e682 100644 --- a/docs/getting-started/whats-new.mdx +++ b/docs/getting-started/whats-new.mdx @@ -1,65 +1,112 @@ --- title: "What's New in FastMCP 4" sidebarTitle: "What's New" -description: A sessionless MCP protocol, the state layer that replaces sessions, and enterprise identity. +description: FastMCP 4 makes stateful MCP applications work on the sessionless protocol while one server serves every protocol era. icon: sparkles --- -FastMCP 4 runs on version 2 of the MCP Python SDK, which rewrote the protocol layer to support MCP's new sessionless protocol, `2026-07-28`. That protocol drives most of this release. It changes how servers deploy, how clients connect, where state lives between calls, and how a running tool asks the user a question. +FastMCP 4 makes stateful MCP applications work on MCP's sessionless protocol. Tools can ask follow-up questions across requests, preserve authenticated user state, and move long-running work into background tasks without sticky sessions or a continuously connected client. -Most FastMCP 3 servers run on 4 unchanged. Two things need attention: `ctx.sample()` and `ctx.list_roots()` are gone, and code that builds MCP protocol models by hand now uses snake_case field names where the SDK used camelCase. [Upgrading from FastMCP 3](/getting-started/upgrading/from-fastmcp-3) covers every break in detail. +The protocol changed completely underneath those APIs. Your application usually does not: one FastMCP server negotiates both protocol eras per connection, and most FastMCP 3 servers upgrade unchanged. + +That is the theme of version 4: stateless transport without stateless application code. The release also makes protocol extensions a first-class surface, adds enterprise identity for agents acting on behalf of users, and strengthens production defaults across caching, routing, and security. FastMCP 4 is in **beta**. Pin an exact version and expect sharp edges. See [Install the v4 prerelease](/getting-started/upgrading/from-fastmcp-3#install-the-v4-prerelease). -## Every protocol era +## Protocol compatibility -A FastMCP 4 server answers clients on both sides of the protocol transition from a single deployment. The SDK negotiates per connection: the sessionless protocol for clients that have moved forward, the session-based handshake for everyone else. You adopt the new protocol without forking your deployment or gating clients by version, which supersedes FastMCP's earlier "latest protocol only" stance. +A protocol migration usually forces a choice between breaking clients that have not moved yet and holding the server back with them. FastMCP 4 serves both eras from one deployment, negotiating the best mutual version for each connection. Modern clients get the sessionless protocol while handshake-era clients continue working unchanged. -Statelessness pays off in how you run the server. A sessionless request carries everything needed to answer it, so any replica behind an ordinary load balancer can serve any request and session affinity stops being a deployment requirement. +Statelessness changes how that deployment scales. Each modern request carries everything needed to answer it, so any replica behind an ordinary load balancer can serve any request and session affinity stops being a requirement. -The client default flipped to match. `Client(url)` probes for the modern protocol and adopts it when the server offers it, where every earlier FastMCP version pinned the handshake outright. +The client default follows the same rule. `Client(url)` probes for the modern protocol and falls back to the handshake when necessary. Pin `mode="legacy"` only when your application specifically needs the session back-channel. ```python from fastmcp import Client -# Probes for the modern protocol, falls back to the handshake +# Negotiate the best mutual protocol client = Client("https://example.com/mcp") -# Pins the handshake, when you need the session back-channel +# Require the handshake-era protocol legacy = Client("https://example.com/mcp", mode="legacy") ``` -That default is what puts the modern capabilities within reach of ordinary client code: a task-enabled tool hands back a handle to poll, and multi-round-trip elicitation resolves across successive requests, with the caller opting in to neither. Once connected, `client.protocol_version`, `client.server_info`, `client.server_capabilities`, and `client.instructions` read the same whichever era you negotiated, so code that inspects a connection never branches on how it was established. See [Protocol negotiation](/clients/client#protocol-negotiation). +Once connected, `client.protocol_version`, `client.server_info`, `client.server_capabilities`, and `client.instructions` expose the same interface whichever era was negotiated. Application code that inspects a server does not need a protocol-version branch. See [Protocol negotiation](/clients/client#protocol-negotiation). -Intermediaries benefit too. On a modern connection, FastMCP's client attaches the method, the target name, and any opted-in argument values as HTTP headers, so a gateway or load balancer can route a request without parsing its JSON-RPC body. See [Gateway Routing Headers](/deployment/http#gateway-routing-headers). +On modern connections, FastMCP also attaches the method, target name, and opted-in argument values as HTTP headers. Gateways and load balancers can route requests without parsing JSON-RPC bodies. See [Gateway routing headers](/deployment/http#gateway-routing-headers). -## Server-to-client requests +## Stateful applications -A sessionless connection gives the server no channel to push a request down to a connected client mid-execution. Three `Context` methods depended on that channel, and this is the one part of FastMCP 4 likely to break an existing server. +The modern protocol removes transport-level sessions, but applications still need conversations, user state, and long-running work. FastMCP moves those concerns into explicit application primitives that survive fresh connections. Shared stores and request-state keys extend them across replicas and worker restarts. -`ctx.sample()`, `ctx.sample_step()`, and `ctx.list_roots()` are removed. Touching one raises `AttributeError` on every era, so the break surfaces when you upgrade rather than in production against whichever client happens to negotiate the modern protocol. +### Interactive tools -For generation, call an LLM directly from your tool: your server holds the API key, creates a provider client, and awaits a completion inline. That works against every client, including the many that never implemented sampling at all, and a tool that chains several generations pays no round trip for any of them. See [Sampling](/servers/sampling). +Many useful tools need more than one exchange. A booking tool asks for a destination, then a date, then confirmation. A destructive operation asks the user to approve it before continuing. -When borrowing the *caller's* model is the actual point, or when a tool genuinely needs the client's roots, the tool asks by returning a description of what it needs. The round completes normally, the client answers, and it re-issues the call with the answer attached. `ctx.elicit()` is untouched and still works on handshake connections; on modern connections that same return-and-resume shape covers elicitation as well. See [the guard pattern](/servers/elicitation#elicitation-on-the-modern-protocol). +On the modern protocol, the tool returns a description of the input it needs. That result completes the request normally. The client fulfils the request and calls the tool again with the answer attached; the tool runs from the top, reads `ctx.input_responses`, and either asks another question or returns its final result. -Logging and progress are unaffected. Both are notifications, and notifications ride the response stream on every era. +Each request completes while the user responds. Single-process servers use an automatic process-local key to protect the state carried between rounds; load-balanced deployments configure one shared key so any replica can validate and resume the next round: -## Session state +```python +import os -If every request arrives on a fresh connection, a tool that wants to remember something between calls has nowhere to keep it. Weighing protocol-level sessions against statelessness, the MCP working group [chose statelessness](https://github.com/modelcontextprotocol/transports-wg/blob/main/docs/sessions-vs-sessionless-decision.md) and moved session semantics up to the application: the server hands out an identifier, and the client passes it back. +from fastmcp import Context, FastMCP +from mcp.server.request_state import RequestStateSecurity +from mcp.types import ElicitRequest, ElicitRequestFormParams, InputRequiredResult -FastMCP implements that pattern and adds the isolation a bare handle lacks. State is stored server-side and keyed to the authenticated user, so a handle is inert in anyone else's hands. +mcp = FastMCP( + "Booking", + request_state_security=RequestStateSecurity( + keys=[os.environ["REQUEST_STATE_KEY"].encode()] + ), +) -Most tools want a single bucket per user. Declare a `UserSession` parameter and FastMCP injects it the way it injects `Context`: it never appears in the tool's input schema, and the caller passes nothing, because the user's identity selects the right bucket. + +@mcp.tool +async def book_flight(ctx: Context) -> str | InputRequiredResult: + answers = ctx.input_responses + if answers is None: + params = ElicitRequestFormParams( + message="Where would you like to fly?", + requested_schema={ + "type": "object", + "properties": {"destination": {"type": "string"}}, + "required": ["destination"], + }, + ) + return InputRequiredResult( + result_type="input_required", + input_requests={ + "destination": ElicitRequest( + method="elicitation/create", + params=params, + ) + }, + ) + + response = answers["destination"] + if response.action != "accept" or response.content is None: + return "Booking cancelled." + + destination = response.content["destination"] + return f"Booked a flight to {destination}." +``` + +Every replica must receive the same `REQUEST_STATE_KEY`, containing at least 32 bytes of secret key material. A FastMCP client drives the loop through its existing elicitation handler, so client code receives the terminal result without managing the intermediate rounds. See [Elicitation on the modern protocol](/servers/elicitation#elicitation-on-the-modern-protocol). + +### Session state + +Application state follows the same explicit model. FastMCP stores state server-side and binds it to the authenticated user, so a session handle is inert in another user's hands. + +Most tools want one state bucket per user. Declare a `UserSession` parameter and FastMCP injects it like `Context`: it never appears in the tool schema, and the caller passes nothing because their authenticated identity selects the bucket. ```python from fastmcp import FastMCP from fastmcp.server.sessions import UserSession -mcp = FastMCP("assistant") +mcp = FastMCP("Assistant") @mcp.tool @@ -70,18 +117,19 @@ async def remember(fact: str, session: UserSession) -> str: return f"Remembered {len(facts)} facts." ``` -Because the bucket is chosen from the caller's identity, `UserSession` requires [authentication](/servers/auth/authentication). An unauthenticated request has no user to key on, so the tool raises rather than guessing at a bucket. +`UserSession` requires [authentication](/servers/auth/authentication), since an unauthenticated request has no user to key on. When one user needs several independent buckets, such as separate carts or conversations, `SessionId` exposes the handle as an explicit string argument. -When one user needs several independent buckets, such as separate carts or parallel conversations, `SessionId` makes the handle an explicit string argument that the agent obtains from `create_session` and supplies on each call. See [Session State](/servers/sessions). +The default in-memory state store is process-local. To preserve state across restarts or share it among replicas, pass a shared persistent `session_state_store`. See [Session state](/servers/sessions). -## Background tasks +### Background work -Long-running work runs as a background task: the server accepts the call and returns a handle immediately, and the client polls for the result while the work proceeds. Tasks left the core MCP spec during the SDK rewrite and returned as the `io.modelcontextprotocol/tasks` extension, which FastMCP implements end to end in the optional `fastmcp-tasks` package. +Long-running tools create a different kind of state problem: holding a request open for several minutes invites timeouts and leaves the user unable to tell whether work is progressing. Background tasks accept the call and return a handle immediately, then let the client poll while work proceeds asynchronously. -`@mcp.tool(task=True)` remains the authoring surface and [Docket](https://github.com/chrisguidry/docket) still provides the durable execution engine, so the wire protocol modernizing underneath costs you no code change. What's new is the registration: tasks arrive as an extension you add to the server. +FastMCP implements the `io.modelcontextprotocol/tasks` extension in the optional `fastmcp-tasks` package. The authoring API remains `@mcp.tool(task=True)`, backed by [Docket](https://github.com/chrisguidry/docket): ```python import asyncio + from fastmcp import FastMCP from fastmcp_tasks import TasksExtension @@ -91,24 +139,28 @@ mcp.add_extension(TasksExtension()) @mcp.tool(task=True) async def slow_computation(duration: int) -> str: - """A long-running operation.""" + """Run a long computation.""" await asyncio.sleep(duration) return f"Completed in {duration} seconds" ``` -A FastMCP client handles the handle-and-poll cycle transparently, so `client.call_tool(...)` looks the same whether or not the call ran in the background. See [Background Tasks](/servers/tasks). +`fastmcp.Client` handles the task handle and polling cycle, so `client.call_tool(...)` returns the same way whether the tool ran inline or in the background. See [Background tasks](/servers/tasks). -## Server extensions +`TasksExtension()` uses an in-memory, single-process backend by default. Configure a Redis or Valkey backend for durable work that survives restarts and runs across separate workers. -Background tasks are the first capability built on a more general one. An MCP extension is a protocol feature named by a reverse-DNS string and negotiated as a capability, and FastMCP 4 makes extensions a first-class surface rather than something only the framework can add. +## Extensible protocol -`FastMCP.add_extension()` lets an extension advertise a capability, add request methods, intercept `tools/call`, and run a lifespan hook, all with full access to the component registry, `Context`, and auth. The same extensions flow through the client with `Client(extensions=...)`. A cross-cutting protocol feature becomes a supported plugin instead of surgery on core, and `TasksExtension` is the worked example of everything the interface allows. See [Server Extensions](/servers/extensions). +Background tasks are built on a general extension surface. An MCP extension advertises a capability under a reverse-DNS identifier and can add behavior negotiated between a server and client. -## Argument completion +### Server extensions -When a client offers autocomplete for a prompt argument or a resource-template parameter, it asks the server which values fit, narrowing the list as the user types. FastMCP 4 lets a server answer. A single `@mcp.completion` handler receives the reference being completed, the argument and its partial value, and the arguments the user has already supplied, and returns the candidates the client surfaces as suggestions. +`FastMCP.add_extension()` lets an extension advertise capabilities, add request methods, intercept `tools/call`, and own lifespan behavior with access to the component registry, `Context`, and authentication. Client extensions use the matching `Client(extensions=...)` interface. -Because the handler sees the earlier arguments, completions can depend on them: a `repo` parameter can suggest only the repositories under the `owner` already chosen. +Cross-cutting protocol behavior can therefore live in a supported plugin instead of requiring changes to FastMCP core. `TasksExtension` is a complete example of the interface. See [Server extensions](/servers/extensions). + +### Argument completion + +FastMCP 4 also lets servers answer MCP argument-completion requests. A completion handler sees the prompt or resource-template argument, its partial value, and values already supplied, so suggestions can depend on earlier choices. ```python from fastmcp import FastMCP @@ -126,74 +178,40 @@ def write_poem(theme: str) -> str: def complete(ref, argument, context): if isinstance(ref, PromptReference) and argument.name == "theme": options = ["nature", "love", "adventure"] - return [o for o in options if o.startswith(argument.value)] + return [option for option in options if option.startswith(argument.value)] return None ``` -Registering a handler advertises the completions capability during negotiation, so a client only sends requests to a server that answers them, identically on both protocol eras. See [Argument Completion](/servers/completions). +Registering the handler advertises the completion capability during negotiation, so clients only send requests to servers that support them. See [Argument completion](/servers/completions). ## Enterprise identity -FastMCP 4 ships a complete server-side implementation of identity assertion, the enterprise "on-behalf-of" flow: a corporate identity provider issues a signed assertion, the user's agent presents it, and the server mints a short-lived token, with no browser login and no per-user consent screen. Behind one parameter on the existing auth providers, FastMCP performs the signature verification, binding checks, replay rejection, and scoped token issuance. +Interactive OAuth authorization assumes a person can complete a browser flow. Internal agents often act for employees without a person waiting at a keyboard, while the server still needs the employee's identity for authorization and audit. + +Identity assertion carries that identity through the agent. A corporate identity provider signs an assertion, the agent presents it, and the server exchanges it for a short-lived token without an interactive login or consent screen. FastMCP performs signature verification, binding checks, replay rejection, and scoped token issuance through the authentication providers you already use. ```python from fastmcp import FastMCP from fastmcp.server.auth import IdentityAssertion, OAuthProxy auth = OAuthProxy( - # existing upstream configuration unchanged - identity_assertion=IdentityAssertion(trusted_issuers=["https://login.acme-corp.com"]), + # Existing upstream configuration + identity_assertion=IdentityAssertion( + trusted_issuers=["https://login.acme-corp.com"] + ), ) mcp = FastMCP("Internal API", auth=auth) ``` -The asserted subject flows into the normal auth context, so tools read it through `get_access_token()` like any other identity. See [Identity Assertion](/servers/auth/oauth-proxy#identity-assertion-sep-990). +The asserted subject enters the normal authentication context, so tools read it through `get_access_token()` like any other identity. See [Identity assertion](/servers/auth/oauth-proxy#identity-assertion-sep-990). -Authorizing a caller by role is a related, provider-agnostic need. Scopes are standardized, so `require_scopes` behaves the same everywhere, but roles and groups are not part of OIDC and every provider files them under a different claim. `require_roles` handles the comparison and takes an `extract` callable naming where to look, so Keycloak's `realm_access.roles`, Cognito's `cognito:groups`, and Auth0's namespaced claims all work without FastMCP guessing. +Authorization gained a provider-neutral role check as well. `require_roles` accepts an extraction function for providers that store roles and groups under different claims, while [scope step-up challenges](/servers/authorization#signaling-scope-shortfalls) tell a client exactly which scopes to request. -```python -from fastmcp import FastMCP -from fastmcp.server.auth import require_roles +For clients with no user behind them, such as backend services and scheduled jobs, `ClientCredentialsOAuthProvider` implements the OAuth 2.0 client-credentials grant with no browser or redirect. See [Machine-to-machine authentication](/clients/auth/client-credentials). -mcp = FastMCP("Internal API") +## Production defaults - -@mcp.tool(auth=require_roles("admin", extract=lambda c: c["realm_access"]["roles"])) -def rotate_credentials() -> str: - """Only callable by a caller holding the 'admin' role.""" - return "Rotated" -``` - -That example shows the check in isolation. Enforcing it for real needs an HTTP-transport server with a token-validating `auth` provider configured, since STDIO has no OAuth concept and skips every check. A `JWTVerifier`, a `RemoteAuthProvider`, or any provider built on one such as `KeycloakAuthProvider` all expose claims directly. See [Authorization](/servers/authorization#require_roles). - -The client side arrived too. Plenty of FastMCP clients have no user behind them, such as a backend service, a scheduled job, or one MCP server calling another. `ClientCredentialsOAuthProvider` authenticates one of those to a protected server with the OAuth 2.0 client-credentials grant: no browser, no redirect, no consent screen. - -```python -import asyncio - -from fastmcp import Client -from fastmcp.client.auth import ClientCredentialsOAuthProvider - -auth = ClientCredentialsOAuthProvider( - client_id="my-client-id", - client_secret="my-client-secret", - scopes=["read", "write"], -) - - -async def main(): - async with Client("https://example.com/mcp", auth=auth) as client: - await client.list_tools() - - -asyncio.run(main()) -``` - -See [Machine-to-Machine Authentication](/clients/auth/client-credentials). - -## Response caching - -A server can stamp freshness hints on its results, and a caching client reuses a result within that window instead of making the round trip. Set the defaults on the server and every response carries them. +A server can now attach freshness hints to its results, and a caching client can reuse those results without another round trip. Set a default time-to-live and scope on the server: ```python from fastmcp import FastMCP @@ -201,10 +219,18 @@ from fastmcp import FastMCP mcp = FastMCP("Weather", cache_ttl=300, cache_scope="public") ``` -Backing the client's cache with the distributed `KeyValueResponseCacheStore` puts it in Redis or any key-value store, so a fleet of clients or proxy replicas shares fills rather than each paying for its own. See [Response caching](/clients/client#response-caching). +`KeyValueResponseCacheStore` can place the client cache in Redis or another key-value store so a fleet of clients or proxies shares fills. See [Response caching](/clients/client#response-caching). -## Security defaults +Resource templates now reject path traversal, absolute paths, and null bytes in their parameters before the handler runs. The protection is enabled by default and applies to mounted and proxied templates. See [Path security](/servers/resources#path-security). -Templated resources now screen their parameters for path traversal, absolute paths, and null bytes before the handler runs. This is on by default and covers mounted and proxied templates too, so a template that interpolates a parameter into a filesystem path no longer has to validate it by hand. See [path security](/servers/resources#path-security). +OAuth defaults also distinguish native clients from web applications during Dynamic Client Registration, and missing scopes now produce an `InsufficientScopeError` that names the scopes required to continue. See [Application type](/servers/auth/oauth-proxy#application-type-web-vs-native) and [scope shortfalls](/servers/authorization#signaling-scope-shortfalls). -The OAuth flow got more precise in two places. Dynamic Client Registration honors a client's declared `application_type`: the permissive loopback and app-scheme callbacks that MCP clients rely on stay the default for `"native"`, while a client registering as `"web"` is held to stricter browser-app redirect rules. And when `AuthMiddleware` denies a call specifically for a missing scope, it raises `InsufficientScopeError` naming which scopes would fix it, so a caller re-authorizes precisely instead of retrying blind. See [Application Type](/servers/auth/oauth-proxy#application-type-web-vs-native) and [Signaling Scope Shortfalls](/servers/authorization#signaling-scope-shortfalls). +## Upgrade note + +The sessionless protocol has no live connection for a server to call back into during execution. FastMCP 4 therefore removes `ctx.sample()`, `ctx.sample_step()`, and `ctx.list_roots()` from every protocol era so incompatible code fails immediately during an upgrade. + +For generation, call an LLM directly from the server when your application owns the model. When borrowing the caller's model is the point, return an `InputRequiredResult` carrying a sampling request and read the answer on the next round. Roots use the same return-and-resume pattern. See [Sampling](/servers/sampling) and [the guard pattern](/servers/elicitation#sampling-and-roots). + +`ctx.elicit()` remains available on handshake-era connections; modern connections use the multi-round pattern described above. Code that constructs MCP protocol models directly must also use snake_case Python field names with SDK v2. + +[Upgrading from FastMCP 3](/getting-started/upgrading/from-fastmcp-3) covers these changes and every other compatibility break. diff --git a/docs/integrations/anthropic.mdx b/docs/integrations/anthropic.mdx index 08b9b2c9c..6fb8841e1 100644 --- a/docs/integrations/anthropic.mdx +++ b/docs/integrations/anthropic.mdx @@ -69,9 +69,11 @@ You'll also need to authenticate with Anthropic. You can do this by setting the export ANTHROPIC_API_KEY="your-api-key" ``` -Here is an example of how to call your server from Python. Note that you'll need to replace `https://your-server-url.com` with the actual URL of your server. In addition, we use `/mcp/` as the endpoint because we deployed a streamable-HTTP server with the default path; you may need to use a different endpoint if you customized your server's deployment. **At this time you must also include the `extra_headers` parameter with the `anthropic-beta` header.** +Here is an example of how to call your server from Python. Note that you'll need to replace `https://your-server-url.com` with the actual URL of your server. In addition, we use `/mcp/` as the endpoint because we deployed a streamable-HTTP server with the default path; you may need to use a different endpoint if you customized your server's deployment. -```python {5, 13-22} +The connector is in beta, so the call goes through `client.beta.messages` with the `mcp-client-2025-11-20` flag. Each entry in `mcp_servers` also needs a matching `mcp_toolset` entry in `tools` that references it by name; declaring the server without the toolset is rejected as a validation error. + +```python {5, 14-23} import anthropic from rich import print @@ -81,8 +83,9 @@ url = 'https://your-server-url.com' client = anthropic.Anthropic() response = client.beta.messages.create( - model="claude-sonnet-4-20250514", + model="claude-sonnet-5", max_tokens=1000, + betas=["mcp-client-2025-11-20"], messages=[{"role": "user", "content": "Roll a few dice!"}], mcp_servers=[ { @@ -91,9 +94,7 @@ response = client.beta.messages.create( "name": "dice-server", } ], - extra_headers={ - "anthropic-beta": "mcp-client-2025-04-04" - } + tools=[{"type": "mcp_toolset", "mcp_server_name": "dice-server"}], ) print(response.content) @@ -193,7 +194,7 @@ Error code: 400 - { To authenticate the client, you can pass the token using the `authorization_token` parameter in your MCP server configuration: -```python {8, 21} +```python {8, 22} import anthropic from rich import print @@ -206,8 +207,9 @@ access_token = 'your-access-token' client = anthropic.Anthropic() response = client.beta.messages.create( - model="claude-sonnet-4-20250514", + model="claude-sonnet-5", max_tokens=1000, + betas=["mcp-client-2025-11-20"], messages=[{"role": "user", "content": "Roll a few dice!"}], mcp_servers=[ { @@ -217,9 +219,7 @@ response = client.beta.messages.create( "authorization_token": access_token } ], - extra_headers={ - "anthropic-beta": "mcp-client-2025-04-04" - } + tools=[{"type": "mcp_toolset", "mcp_server_name": "dice-server"}], ) print(response.content) diff --git a/docs/integrations/github.mdx b/docs/integrations/github.mdx index d493eb1ef..d1a2a3608 100644 --- a/docs/integrations/github.mdx +++ b/docs/integrations/github.mdx @@ -69,7 +69,7 @@ from fastmcp.server.auth.providers.github import GitHubProvider # The GitHubProvider handles GitHub's token format and validation auth_provider = GitHubProvider( client_id="Ov23liAbcDefGhiJkLmN", # Your GitHub OAuth App Client ID - client_secret="github_pat_...", # Your GitHub OAuth App Client Secret + client_secret="your-github-client-secret", # Your GitHub OAuth App Client Secret base_url="http://localhost:8000", # Must match your OAuth App configuration # redirect_path="/auth/callback" # Default value, customize if needed ) @@ -151,7 +151,7 @@ from cryptography.fernet import Fernet # Production setup with encrypted persistent token storage auth_provider = GitHubProvider( client_id="Ov23liAbcDefGhiJkLmN", - client_secret="github_pat_...", + client_secret="your-github-client-secret", base_url="https://your-production-domain.com", # Production token management diff --git a/docs/integrations/mcp-json-configuration.mdx b/docs/integrations/mcp-json-configuration.mdx index fec8ffc01..b516c9954 100644 --- a/docs/integrations/mcp-json-configuration.mdx +++ b/docs/integrations/mcp-json-configuration.mdx @@ -70,7 +70,7 @@ An object containing environment variables to set when launching the server. All This format is widely adopted across the MCP ecosystem: -- **Claude Desktop**: Uses `~/.claude/claude_desktop_config.json` +- **Claude Desktop**: Uses `~/Library/Application Support/Claude/claude_desktop_config.json` on macOS, `%APPDATA%\Claude\claude_desktop_config.json` on Windows - **Cursor**: Uses `~/.cursor/mcp.json` - **VS Code**: Uses workspace `.vscode/mcp.json` - **Other clients**: Many MCP-compatible applications follow this standard @@ -457,7 +457,7 @@ The generated configuration works with any MCP-compatible application: **Prefer [`fastmcp install claude-desktop`](/integrations/claude-desktop)** for automatic installation. Use MCP JSON for advanced configuration needs. -Copy the `mcpServers` object into `~/.claude/claude_desktop_config.json` +Copy the `mcpServers` object into Claude Desktop's config file (`~/Library/Application Support/Claude/claude_desktop_config.json` on macOS, `%APPDATA%\Claude\claude_desktop_config.json` on Windows) ### Cursor diff --git a/docs/integrations/permit.mdx b/docs/integrations/permit.mdx index ddda7cd2c..66b8c896e 100644 --- a/docs/integrations/permit.mdx +++ b/docs/integrations/permit.mdx @@ -300,10 +300,12 @@ For advanced configuration options and custom middleware extensions, see [Advanc See the [example server](https://github.com/permitio/permit-fastmcp/blob/main/permit_fastmcp/example_server/example.py) for a full implementation with JWT-based authentication. For additional examples and usage patterns, see [Example Server](https://github.com/permitio/permit-fastmcp/blob/main/permit_fastmcp/example_server/): ```python +import os +import datetime + +import jwt from fastmcp import FastMCP, Context from permit_fastmcp.middleware.middleware import PermitMcpMiddleware -import jwt -import datetime # Configure JWT identity extraction os.environ["PERMIT_MCP_IDENTITY_MODE"] = "jwt" diff --git a/docs/servers/auth/oauth-proxy.mdx b/docs/servers/auth/oauth-proxy.mdx index 5cc0f633a..dd4751350 100644 --- a/docs/servers/auth/oauth-proxy.mdx +++ b/docs/servers/auth/oauth-proxy.mdx @@ -283,10 +283,11 @@ auth = OAuthProxy(..., client_storage=MemoryStore()) - Secret used to sign FastMCP JWT tokens issued to clients. Accepts any string or bytes - will be derived into a proper 32-byte cryptographic key using HKDF. + Secret used to sign FastMCP JWT tokens issued to clients. How the key is derived depends on what you pass: - **Default behavior (`None`):** - Derives a 32-byte key using PBKDF2 from the upstream client secret. + - **`bytes`** are used as-is, with no stretching, so supply at least 32 bytes of high-entropy key material. With the default file-backed client storage, the bytes must also decode as UTF-8; use `secrets.token_urlsafe(32).encode()` instead of raw `secrets.token_bytes()`, or configure `client_storage` explicitly. + - **A string** is stretched into a 32-byte key with PBKDF2 (1,000,000 iterations), since a supplied string may be low-entropy. Strings shorter than 12 characters also log a warning. + - **`None`** (the default) derives a 32-byte key from the upstream client secret using HKDF. **For production:** Provide an explicit secret (e.g., from environment variable) to use a fixed key instead of the key derived from the upstream client secret. This allows you to manage keys securely in cloud environments, allows keys to work across multiple instances, and allows you to rotate keys without losing client registrations. diff --git a/docs/servers/auth/oidc-proxy.mdx b/docs/servers/auth/oidc-proxy.mdx index 763963858..006efc8d6 100644 --- a/docs/servers/auth/oidc-proxy.mdx +++ b/docs/servers/auth/oidc-proxy.mdx @@ -155,7 +155,7 @@ Set this if your provider requires a specific authentication method and the defa - Secret used to sign FastMCP JWT tokens issued to clients. Accepts any string or bytes - will be derived into a proper 32-byte cryptographic key using HKDF. + Secret used to sign FastMCP JWT tokens issued to clients. **`bytes`** are used as-is, with no stretching, so supply at least 32 bytes of high-entropy key material. With the default file-backed client storage, the bytes must also decode as UTF-8; use `secrets.token_urlsafe(32).encode()` instead of raw `secrets.token_bytes()`, or configure `client_storage` explicitly. **A string** is stretched into a 32-byte key with PBKDF2 (1,000,000 iterations), since a supplied string may be low-entropy. **Default behavior (`None`):** The key is deterministically derived from `client_secret` using HKDF, on every platform. Because the derivation is deterministic, the same key is produced across restarts as long as `client_secret` doesn't change, so tokens remain valid without any extra configuration. This convenience makes it **only** suitable for development and local testing. diff --git a/docs/servers/auth/remote-oauth.mdx b/docs/servers/auth/remote-oauth.mdx index 957cad053..e2fd14b98 100644 --- a/docs/servers/auth/remote-oauth.mdx +++ b/docs/servers/auth/remote-oauth.mdx @@ -116,8 +116,6 @@ auth = RemoteAuthProvider( token_verifier=token_verifier, authorization_servers=[AnyHttpUrl("https://auth.yourcompany.com")], base_url="https://api.yourcompany.com", # Your server base URL - # Optional: restrict allowed client redirect URIs - allowed_client_redirect_uris=["http://localhost:*", "http://127.0.0.1:*"] ) mcp = FastMCP(name="Company API", auth=auth) @@ -216,13 +214,7 @@ WorkOS's support for Dynamic Client Registration makes it particularly well-suit ## Client Redirect URI Security -`RemoteAuthProvider` also supports the `allowed_client_redirect_uris` parameter for controlling which redirect URIs are accepted from MCP clients during DCR: - -- `None` (default): Broad DCR-compatible redirect support, while rejecting unsafe browser schemes such as `javascript:`, `data:`, `file:`, and `vbscript:` -- Custom list: Specify allowed patterns with wildcard support -- Empty list `[]`: No redirect URIs allowed - -This provides defense-in-depth even though DCR providers typically validate redirect URIs themselves. +Redirect URIs are validated by the DCR provider itself, since it owns the registration flow. To constrain them from the FastMCP side, use [`OAuthProxy`](/servers/auth/oauth-proxy), whose `allowed_client_redirect_uris` parameter accepts a list of allowed patterns with wildcard support. ## Implementation Considerations diff --git a/docs/servers/middleware.mdx b/docs/servers/middleware.mdx index af7d17e42..dd841f00d 100644 --- a/docs/servers/middleware.mdx +++ b/docs/servers/middleware.mdx @@ -378,7 +378,7 @@ mcp.add_middleware(LoggingMiddleware( | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `include_payloads` | `bool` | `False` | Log request/response content | -| `max_payload_length` | `int` | `500` | Truncate payloads beyond this length | +| `max_payload_length` | `int` | `1000` | Truncate payloads beyond this length | | `logger` | `Logger` | module logger | Custom logger instance | ### Timing @@ -533,7 +533,7 @@ mcp.add_middleware(ErrorHandlingMiddleware( | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `include_traceback` | `bool` | `False` | Include stack traces in logs | -| `transform_errors` | `bool` | `False` | Convert exceptions to MCP errors | +| `transform_errors` | `bool` | `True` | Convert exceptions to MCP errors | | `error_callback` | `Callable` | `None` | Custom callback on errors | For automatic retries: diff --git a/docs/servers/prompts.mdx b/docs/servers/prompts.mdx index ae7172522..1986a50bf 100644 --- a/docs/servers/prompts.mdx +++ b/docs/servers/prompts.mdx @@ -448,14 +448,14 @@ A prompt can ask the client for information before it renders. On an MCP 2026-07 -You can configure how the FastMCP server handles attempts to register multiple prompts with the same name. Use the `on_duplicate_prompts` setting during `FastMCP` initialization. +You can configure how the FastMCP server handles attempts to register the same prompt twice. Identity is the component's type, name, and version together, so a prompt may share a name with a tool, and two versions of one prompt coexist. The `on_duplicate` setting covers every component type, so it applies to prompts alongside tools and resources. ```python from fastmcp import FastMCP mcp = FastMCP( name="PromptServer", - on_duplicate_prompts="error" # Raise an error if a prompt name is duplicated + on_duplicate="error" # Raise an error on an exact duplicate ) @mcp.prompt diff --git a/docs/servers/resources.mdx b/docs/servers/resources.mdx index e0daf6e76..13a5d986a 100644 --- a/docs/servers/resources.mdx +++ b/docs/servers/resources.mdx @@ -78,7 +78,7 @@ mcp = FastMCP(name="DataServer") ) def get_application_status() -> str: """Internal function description (ignored if description is provided above).""" - return json.dumps({"status": "ok", "uptime": 12345, "version": mcp.settings.version}) + return json.dumps({"status": "ok", "uptime": 12345, "version": "2.1"}) ``` @@ -793,14 +793,14 @@ A resource or resource template can ask the client for information before it pro -You can configure how the FastMCP server handles attempts to register multiple resources or templates with the same URI. Use the `on_duplicate_resources` setting during `FastMCP` initialization. +You can configure how the FastMCP server handles attempts to register the same resource or template twice. Identity is the component's type, URI, and version together, so two versions of one resource coexist and only an exact repeat collides. The `on_duplicate` setting covers every component type, so it applies to resources and templates alongside tools and prompts. ```python from fastmcp import FastMCP mcp = FastMCP( name="ResourceServer", - on_duplicate_resources="error" # Raise error on duplicates + on_duplicate="error" # Raise an error on an exact duplicate ) @mcp.resource("data://config") diff --git a/docs/servers/tools.mdx b/docs/servers/tools.mdx index e87ebedd4..4a9c4918d 100644 --- a/docs/servers/tools.mdx +++ b/docs/servers/tools.mdx @@ -1081,22 +1081,22 @@ For full documentation on the Context object and all its capabilities, see the [ -You can control how the FastMCP server behaves if you try to register multiple tools with the same name. This is configured using the `on_duplicate_tools` argument when creating the `FastMCP` instance. +You can control how the FastMCP server behaves if you register the same component twice. Identity is the component's type, name, and version together, so a tool and a prompt may share a name, and two versions of one tool coexist. Only an exact repeat of all three counts as a duplicate. The `on_duplicate` argument sets that policy once for every component type. ```python from fastmcp import FastMCP mcp = FastMCP( name="StrictServer", - # Configure behavior for duplicate tool names - on_duplicate_tools="error" + # Configure behavior for exact component duplicates + on_duplicate="error" ) @mcp.tool def my_tool(): return "Version 1" # This will now raise a ValueError because 'my_tool' already exists -# and on_duplicate_tools is set to "error". +# and on_duplicate is set to "error". # @mcp.tool # def my_tool(): return "Version 2" ``` diff --git a/docs/tutorials/mcp.mdx b/docs/tutorials/mcp.mdx index fd3995fff..34b1c86c2 100644 --- a/docs/tutorials/mcp.mdx +++ b/docs/tutorials/mcp.mdx @@ -21,7 +21,7 @@ The answer lies in **standardization**. The AI ecosystem is fragmented. Every mo 1. **Interoperability:** Build one MCP server, and it can be used by any MCP-compliant client (Claude, Gemini, OpenAI, custom agents, etc.) without custom integration code. This is the protocol's most important promise. 2. **Discoverability:** Clients can dynamically ask a server what it's capable of at runtime. They receive a structured, machine-readable "menu" of tools and resources. -3. **Security & Safety:** MCP provides a clear, sandboxed boundary. An LLM can't execute arbitrary code on your server; it can only *request* to run the specific, typed, and validated functions you explicitly expose. +3. **Explicit boundaries:** MCP gives hosts and servers a typed inventory of the capabilities they expose. That creates a clear place to apply authorization, user confirmation, input validation, and sandboxing; the protocol defines the interface, while your application supplies those security policies. 4. **Composability:** You can build small, specialized MCP servers and combine them to create powerful, complex applications. ## Core MCP Components @@ -111,10 +111,6 @@ def summarize_text(text_to_summarize: str) -> str: ## Advanced Capabilities -Beyond the core components, MCP also supports more advanced interaction patterns, such as a server requesting that the *client's* LLM generate a completion (known as **sampling**), or a server sending asynchronous **notifications** to a client. These features enable more complex, bidirectional workflows and are fully supported by FastMCP. +Beyond tools, resources, and prompts, MCP supports richer interaction patterns such as notifications, progress updates, user elicitation, and argument completion. Extensions add capabilities such as durable background tasks. -## Next Steps - -Now that you understand the core concepts of the Model Context Protocol, you're ready to start building. The best place to begin is our step-by-step tutorial. - -[**Tutorial: How to Create an MCP Server in Python →**](/tutorials/create-mcp-server) +FastMCP exposes these patterns through typed Python APIs. For example, [elicitation](/servers/elicitation) lets tools request missing information or confirmation, while [background tasks](/servers/tasks) let long-running work continue after the original request returns. diff --git a/docs/updates.mdx b/docs/updates.mdx index bb66069e9..e930cb36c 100644 --- a/docs/updates.mdx +++ b/docs/updates.mdx @@ -11,11 +11,13 @@ title="FastMCP v4.0.0b1: Fourgone Conclusion" href="https://github.com/PrefectHQ/fastmcp/releases/tag/v4.0.0b1" cta="Read the release notes" > -FastMCP 4 rebuilds the framework on the MCP Python SDK v2, and this beta is the first release to run on the SDK's stable 2.0. The engine underneath changed completely, but FastMCP absorbs nearly all of it — most FastMCP 3 servers run untouched. +FastMCP 4 makes stateful MCP applications work on the sessionless protocol while one deployment continues serving handshake-era clients. The engine underneath changed completely, but FastMCP absorbs nearly all of it — most FastMCP 3 servers upgrade untouched. 🌐 **Every protocol era** — one server answers both the sessionless `2026-07-28` protocol and the older session-based handshake, negotiated per connection. -💾 **State without a session** — `UserSession` and `SessionId` give tools durable state on a protocol that deliberately has none, keyed per user when the request is authenticated. +💬 **Interactive tools** — tools ask follow-up questions across complete request-response rounds, with shared request-state keys for load balancing and worker restarts. + +💾 **State without a session** — `UserSession` and `SessionId` give tools explicit server-side state on a protocol that deliberately has none, keyed per user when the request is authenticated. ⏳ **Background tasks** — the `io.modelcontextprotocol/tasks` extension in the new `fastmcp-tasks` package, on the same Docket engine FastMCP 3 used. diff --git a/fastmcp_slim/fastmcp/server/auth/oauth_proxy/proxy.py b/fastmcp_slim/fastmcp/server/auth/oauth_proxy/proxy.py index 422ed3b49..2ca7fa311 100644 --- a/fastmcp_slim/fastmcp/server/auth/oauth_proxy/proxy.py +++ b/fastmcp_slim/fastmcp/server/auth/oauth_proxy/proxy.py @@ -386,7 +386,7 @@ class OAuthProxy(OAuthProvider, ConsentMixin): If None, an encrypted file store will be created in the data directory. jwt_signing_key: Secret for signing FastMCP JWT tokens (any string or bytes). If bytes are provided, they will be used as-is. - If a string is provided, it will be derived into a 32-byte key using PBKDF2 (1.2M iterations). + If a string is provided, it will be derived into a 32-byte key using PBKDF2 (1,000,000 iterations). If not provided, it will be derived from the upstream client secret using HKDF. require_authorization_consent: Consent screen behavior (default True). - True: always show the consent screen before redirecting to the From a22f778dbf8303fda4315fbb512373d959454961 Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Wed, 29 Jul 2026 09:51:22 -0400 Subject: [PATCH 20/53] Rewrite the FastMCP docs welcome page (#4709) --- README.md | 15 ++-- docs/deployment/prefect-horizon.mdx | 2 +- docs/docs.json | 2 +- docs/getting-started/welcome.mdx | 104 ++++++++++++------------- docs/v3/deployment/prefect-horizon.mdx | 2 +- 5 files changed, 60 insertions(+), 65 deletions(-) diff --git a/README.md b/README.md index 5d312c0b3..920996af9 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ --- -The [Model Context Protocol](https://modelcontextprotocol.io/) (MCP) connects LLMs to tools and data. FastMCP gives you everything you need to go from prototype to production: +The [Model Context Protocol](https://modelcontextprotocol.io/) (MCP) connects LLMs to tools and data. FastMCP is a full MCP application framework for servers, clients, and interactive apps. A server starts with ordinary Python: ```python from fastmcp import FastMCP @@ -82,11 +82,11 @@ FastMCP has three pillars: Ready to build? Start with the [installation guide](https://gofastmcp.com/getting-started/installation) or jump straight to the [quickstart](https://gofastmcp.com/getting-started/quickstart). -## Run FastMCP in production with Horizon +## Scale MCP with Horizon -FastMCP is the standard way to build MCP servers. **[Prefect Horizon](https://www.prefect.io/horizon?utm_source=github&utm_medium=readme&utm_campaign=readme_horizon&utm_content=readme_body)** is the enterprise MCP gateway for running them safely. +FastMCP handles the MCP application layer. **[Prefect Horizon](https://www.prefect.io/horizon?utm_source=github&utm_medium=readme&utm_campaign=readme_horizon&utm_content=readme_body)** is the enterprise MCP gateway for scaling servers and tools across teams, with centralized governance over how they are deployed, discovered, secured, and used. -Built by the FastMCP team, Horizon packages the best practices we've learned shipping the world's most popular MCP framework. +FastMCP and Horizon are built by the same team at [Prefect](https://www.prefect.io/). Deploy FastMCP servers from GitHub with branch previews and instant rollback. Create a private registry of every MCP your company uses. Secure access with SSO and tool-level RBAC. Get audit logs, observability, and governance across your MCP stack. Remix approved tools into purpose-built endpoints for teams and agents. @@ -94,10 +94,10 @@ Start with FastMCP. [Scale with Horizon →](https://www.prefect.io/horizon?utm_ ## Installation -We recommend installing FastMCP with [uv](https://docs.astral.sh/uv/): +We recommend adding FastMCP to your project with [uv](https://docs.astral.sh/uv/): ```bash -uv pip install fastmcp +uv add fastmcp ``` For full installation instructions, including verification and upgrading, see the [**Installation Guide**](https://gofastmcp.com/getting-started/installation). @@ -108,9 +108,6 @@ For full installation instructions, including verification and upgrading, see th - [Upgrading from MCP SDK v1](https://gofastmcp.com/getting-started/upgrading/from-mcp-sdk-v1) or [v2](https://gofastmcp.com/getting-started/upgrading/from-mcp-sdk-v2) - [Upgrading from the low-level SDK v1](https://gofastmcp.com/getting-started/upgrading/from-low-level-sdk-v1) or [v2](https://gofastmcp.com/getting-started/upgrading/from-low-level-sdk-v2) -> [!NOTE] -> If `import fastmcp` fails right after a `pip` upgrade from FastMCP 3.2 or earlier, run `pip install --force-reinstall fastmcp`. See [Troubleshooting](https://gofastmcp.com/getting-started/installation#troubleshooting) for why this happens (`uv` is unaffected). - ## 📚 Documentation FastMCP's complete documentation is available at **[gofastmcp.com](https://gofastmcp.com)**, including detailed guides, API references, and advanced patterns. diff --git a/docs/deployment/prefect-horizon.mdx b/docs/deployment/prefect-horizon.mdx index b22644181..68f157c52 100644 --- a/docs/deployment/prefect-horizon.mdx +++ b/docs/deployment/prefect-horizon.mdx @@ -5,7 +5,7 @@ description: The MCP platform from the FastMCP team icon: cloud --- -[Prefect Horizon](https://www.prefect.io/horizon) is a platform for deploying and managing MCP servers. Built by the FastMCP team at [Prefect](https://www.prefect.io), Horizon provides managed hosting, authentication, access control, and a registry of MCP capabilities. +[Prefect Horizon](https://www.prefect.io/horizon?utm_source=gofastmcp&utm_medium=docs&utm_campaign=docs_horizon&utm_content=guide_intro) is a platform for deploying and managing MCP servers. Built by the FastMCP team at [Prefect](https://www.prefect.io), Horizon provides managed hosting, authentication, access control, and a registry of MCP capabilities. Horizon includes a **free personal tier for FastMCP users**, making it the fastest way to get a secure, production-ready server URL with built-in OAuth authentication. diff --git a/docs/docs.json b/docs/docs.json index a3490a88a..c52daada0 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -67,7 +67,7 @@ "label": "" }, { - "href": "https://prefect.io/horizon", + "href": "https://www.prefect.io/horizon?utm_source=gofastmcp&utm_medium=docs&utm_campaign=docs_horizon&utm_content=header", "icon": "cloud", "label": "Prefect Horizon" } diff --git a/docs/getting-started/welcome.mdx b/docs/getting-started/welcome.mdx index 1e3fe2a9d..5c13e6bdf 100644 --- a/docs/getting-started/welcome.mdx +++ b/docs/getting-started/welcome.mdx @@ -1,26 +1,11 @@ --- -title: "Welcome to FastMCP" +title: "FastMCP: The Framework for MCP" sidebarTitle: "Welcome!" -description: The fast, Pythonic way to build MCP servers, clients, and applications. +description: FastMCP is the standard framework for building Model Context Protocol (MCP) servers, clients, and interactive applications. icon: hand-wave mode: center --- -{/* 'F' logo on a watercolor background - 'F' logo on a watercolor background - - - */} +**FastMCP is a full framework for building [Model Context Protocol](https://modelcontextprotocol.io/) (MCP) applications.** It gives you one coherent API for servers, clients, and interactive apps. Use it to expose Python functions as MCP tools, connect to local or remote MCP servers, and return interactive interfaces directly from your tools. FastMCP manages schema generation, validation, transport, authentication, and protocol compatibility around your application code. -**FastMCP is the standard framework for building MCP applications.** The [Model Context Protocol](https://modelcontextprotocol.io/) (MCP) connects LLMs to tools and data. FastMCP gives you everything you need to go from prototype to production — build servers that expose capabilities, connect clients to any MCP service, and give your tools interactive UIs: +A FastMCP server starts with ordinary Python: ```python {1} from fastmcp import FastMCP mcp = FastMCP("Demo 🚀") + @mcp.tool def add(a: int, b: int) -> int: - """Add two numbers""" + """Add two numbers.""" return a + b + if __name__ == "__main__": mcp.run() ``` +## Move fast and make things -## Move Fast and Make Things +An effective MCP application needs more than a function registry. Models need accurate schemas, callers need validated results, clients need compatible transports, and production servers need authentication and predictable lifecycle management. -The [Model Context Protocol](https://modelcontextprotocol.io/) (MCP) lets you give agents access to your tools and data. But building an effective MCP application is harder than it looks. +FastMCP treats those as framework responsibilities. Declare a Python function and FastMCP derives its schema, validates its inputs and outputs, and exposes it through MCP. Connect a client to a URL and FastMCP handles protocol negotiation, authentication, and connection lifecycle. Your application remains ordinary Python while FastMCP keeps the MCP boundary correct. -FastMCP handles all of it. Declare a tool with a Python function, and the schema, validation, and documentation are generated automatically. Connect to a server with a URL, and transport negotiation, authentication, and protocol lifecycle are managed for you. You focus on your logic, and the MCP part just works: **with FastMCP, best practices are built in.** +**That's why FastMCP is the standard framework for working with MCP.** FastMCP created the high-level Python API incorporated into the official MCP Python SDK in 2024. The actively maintained standalone project is now downloaded more than a million times a day, and some version of FastMCP powers 70% of MCP servers across all languages. -**That's why FastMCP is the standard framework for working with MCP.** FastMCP 1.0 was incorporated into the official MCP Python SDK in 2024. Today, the actively maintained standalone project is downloaded a million times a day, and some version of FastMCP powers 70% of MCP servers across all languages. +## Servers, clients, and apps -FastMCP has three pillars: +FastMCP covers the full MCP application lifecycle through three complementary pillars: - Expose tools, resources, and prompts to LLMs. + Expose Python functions, data, and instructions as MCP tools, resources, and prompts. - Give your tools interactive UIs rendered directly in the conversation. + Give MCP tools interactive user interfaces rendered directly in the conversation. - Connect to any MCP server — local or remote, programmatic or CLI. + Connect to any MCP server through Python, the command line, or another MCP application. -**[Servers](/servers/server)** wrap your Python functions into MCP-compliant tools, resources, and prompts. **[Clients](/clients/client)** connect to any server with full protocol support. And **[Apps](/apps/overview)** give your tools interactive UIs rendered directly in the conversation. +**[Servers](/servers/server)** turn your application logic into MCP capabilities with generated schemas and validation. **[Clients](/clients/client)** connect to local or remote MCP servers with full protocol support. **[Apps](/apps/overview)** let tools return forms, tables, charts, and other interactive interfaces alongside ordinary MCP results. -**Building in TypeScript?** [FastMCP for TypeScript](https://github.com/PrefectHQ/fastmcp-ts) is the official counterpart, built and maintained by the same team. The three pillars work the same way there, so what you learn here carries over. +The three pillars share one model: FastMCP owns the protocol machinery while your code defines what the application does. -Ready to build? Start with the [installation guide](/getting-started/installation) or jump straight to the [quickstart](/getting-started/quickstart). +**Building in TypeScript?** [FastMCP for TypeScript](https://github.com/PrefectHQ/fastmcp-ts) is the official counterpart, built and maintained by the same team. Its servers, clients, and apps follow the same concepts, so what you learn here carries over. + + + + Add FastMCP to your project with `uv add fastmcp`, verify the package, and find the right upgrade guide. + + + Create a tool, run its server, call it from a client, and add an interactive UI. + + FastMCP is made with 💙 by [Prefect](https://www.prefect.io/). -## Run FastMCP in production with Horizon + +**This documentation reflects FastMCP's `main` branch**, so it may describe features that have not reached a stable release. Version badges identify when features were introduced. + -FastMCP is the standard way to build MCP servers. **[Prefect Horizon](https://www.prefect.io/horizon?utm_source=gofastmcp&utm_medium=docs&utm_campaign=docs_welcome&utm_content=welcome_body)** is the enterprise MCP gateway for running them safely. +## Scale MCP with Horizon -Built by the FastMCP team, Horizon packages the best practices we've learned shipping the world's most popular MCP framework. +FastMCP handles the MCP application layer. **[Prefect Horizon](https://www.prefect.io/horizon?utm_source=gofastmcp&utm_medium=docs&utm_campaign=docs_welcome&utm_content=welcome_body)** is the enterprise MCP gateway for scaling servers and tools across teams, with centralized governance over how they are deployed, discovered, secured, and used. -Deploy FastMCP servers from GitHub with branch previews and instant rollback. Create a private registry of every MCP your company uses. Secure access with SSO and tool-level RBAC. Get audit logs, observability, and governance across your MCP stack. Remix approved tools into purpose-built endpoints for teams and agents. +Horizon applies the operational patterns developed while maintaining FastMCP: deploy servers from GitHub with branch previews and instant rollback, organize them in a private registry, protect access with SSO and tool-level RBAC, and observe activity through audit logs and telemetry. + +Horizon can also combine approved tools into purpose-built MCP endpoints for different teams and agents, while keeping access policy and governance centralized. Start with FastMCP. [Scale with Horizon →](https://www.prefect.io/horizon?utm_source=gofastmcp&utm_medium=docs&utm_campaign=docs_welcome&utm_content=welcome_cta) - -**This documentation reflects FastMCP's `main` branch**, meaning it always reflects the latest development version. Features are generally marked with version badges (e.g. `New in version: 3.0.0`) to indicate when they were introduced. Note that this may include features that are not yet released. - +## LLM-friendly docs -## LLM-Friendly Docs +FastMCP documentation is designed for developers and coding agents. Every page is available as Markdown, the complete documentation is published in `llms.txt` formats, and the documentation itself is exposed through an MCP server. -The FastMCP documentation is available in multiple LLM-friendly formats: +### MCP server -### MCP Server - -The FastMCP docs are accessible via MCP! The server URL is `https://gofastmcp.com/mcp`. - -In fact, you can use FastMCP to search the FastMCP docs: +Point any MCP-compatible agent at `https://gofastmcp.com/mcp` to let it search the documentation as it works. You can also connect with FastMCP's Python client directly: ```python import asyncio + from fastmcp import Client -async def main(): + +async def main() -> None: async with Client("https://gofastmcp.com/mcp") as client: result = await client.call_tool( name="search_fast_mcp", - arguments={"query": "deploy a FastMCP server"} + arguments={"query": "deploy a FastMCP server"}, ) - print(result) + print(result) + asyncio.run(main()) ``` -### Text Formats +### Markdown formats -The docs are also available in [llms.txt format](https://llmstxt.org/): -- [llms.txt](https://gofastmcp.com/llms.txt) - A sitemap listing all documentation pages -- [llms-full.txt](https://gofastmcp.com/llms-full.txt) - The entire documentation in one file (may exceed context windows) +The documentation is also available in [`llms.txt`](https://llmstxt.org/) formats: -Any page can be accessed as markdown by appending `.md` to the URL. For example, this page becomes `https://gofastmcp.com/getting-started/welcome.md`. +- [`llms.txt`](https://gofastmcp.com/llms.txt) lists every documentation page. +- [`llms-full.txt`](https://gofastmcp.com/llms-full.txt) contains the complete documentation in one file and may exceed some context windows. -You can also copy any page as markdown by pressing "Cmd+C" (or "Ctrl+C" on Windows) on your keyboard. +Append `.md` to any documentation URL to retrieve that page as Markdown. For example, this page is available at `https://gofastmcp.com/getting-started/welcome.md`. You can also copy the current page as Markdown by pressing `Cmd+C` or `Ctrl+C`. diff --git a/docs/v3/deployment/prefect-horizon.mdx b/docs/v3/deployment/prefect-horizon.mdx index b22644181..6a26fa19e 100644 --- a/docs/v3/deployment/prefect-horizon.mdx +++ b/docs/v3/deployment/prefect-horizon.mdx @@ -5,7 +5,7 @@ description: The MCP platform from the FastMCP team icon: cloud --- -[Prefect Horizon](https://www.prefect.io/horizon) is a platform for deploying and managing MCP servers. Built by the FastMCP team at [Prefect](https://www.prefect.io), Horizon provides managed hosting, authentication, access control, and a registry of MCP capabilities. +[Prefect Horizon](https://www.prefect.io/horizon?utm_source=gofastmcp&utm_medium=docs&utm_campaign=docs_horizon&utm_content=v3_guide_intro) is a platform for deploying and managing MCP servers. Built by the FastMCP team at [Prefect](https://www.prefect.io), Horizon provides managed hosting, authentication, access control, and a registry of MCP capabilities. Horizon includes a **free personal tier for FastMCP users**, making it the fastest way to get a secure, production-ready server URL with built-in OAuth authentication. From 07f6eafd99e888f9a0ac61fc30f904f30a5d3f65 Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Wed, 29 Jul 2026 09:51:36 -0400 Subject: [PATCH 21/53] Add comprehensive Codex code review rules (#4710) --- CLAUDE.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index e6357e06b..5a4ef8bed 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -184,6 +184,20 @@ Because the docs land *before* the tag exists, derive the entry from the maintai - **Style:** Prose over code comments for important information - **Docstrings:** FastMCP docstrings are automatically compiled into MDX documents. Use markdown (single backticks, fenced code blocks), not RST (no double backticks). Bare `{}` in examples will be interpreted as JSX — wrap in backticks instead. +## Code Review Rules + +### Framework regressions and root causes + +- Review changes carefully for regressions in supported framework behavior, including interactions beyond the immediate diff. Trace relevant callers, shared abstractions, protocol and public API contracts, and all affected MCP component types. Determine whether a change fixes the causal code path or merely compensates for the symptom; side channels and special cases that leave the root cause intact should be treated as suspect. + +### Comprehensive first pass + +- Review the entire pull request diff against the merge base, not only the latest commits. Inspect every changed file and the relevant surrounding code, collect all independent, substantiated consequential findings before submitting the review, and report the complete set in one review whenever possible. Do not stop after finding the first few issues or defer other already-visible findings to later review cycles. + +### Prior discussion and proportionality + +- When prior review threads and author or maintainer replies are available, read them before commenting. Evaluate responses on their merits and do not repeat a resolved or convincingly rebutted finding without new evidence. Avoid fixating on speculative edge cases: report an edge case only when it is reachable under supported usage or a credible threat model and has meaningful impact; otherwise omit it or clearly treat it as non-blocking. + ## Critical Patterns - Never use bare `except` - be specific with exception types From 44c0907dda78618f882b08de26e31c4428bd74b8 Mon Sep 17 00:00:00 2001 From: nate nowack Date: Wed, 29 Jul 2026 10:22:17 -0500 Subject: [PATCH 22/53] Give parallel Windows CI more timeout headroom (#4680) pytest-timeout falls back to its thread method on Windows, which os._exit()s the process instead of failing the test. A single slow test therefore kills an xdist worker and fails whichever unrelated test it was running. Co-authored-by: Claude Opus 5 (1M context) --- .github/actions/run-pytest/action.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.github/actions/run-pytest/action.yml b/.github/actions/run-pytest/action.yml index e7ee2b8ec..c82e9c0bd 100644 --- a/.github/actions/run-pytest/action.yml +++ b/.github/actions/run-pytest/action.yml @@ -46,6 +46,16 @@ runs: PARALLEL_FLAGS="--numprocesses auto --maxprocesses $MAX_PROCS --dist worksteal" fi + # pytest-timeout has no signal-based method on Windows, so it falls back + # to the thread method, which dumps stacks and os._exit()s the process. + # Under a contended runner that turns a single slow test into a dead + # xdist worker, failing whichever unrelated test that worker happened to + # be running. Give parallel Windows runs more headroom so ordinary + # scheduling jitter does not take a worker down. + if [ "$RUNNER_OS" == "Windows" ] && [ "$MAX_PROCS" != "0" ]; then + TIMEOUT=$((TIMEOUT * 4)) + fi + uv run --no-sync pytest \ --inline-snapshot=disable \ --timeout=$TIMEOUT \ From 0f18a258d4d88d2a112a2e2d647edb2c63246cd4 Mon Sep 17 00:00:00 2001 From: Nicholas Brown Date: Thu, 30 Jul 2026 09:32:58 -0400 Subject: [PATCH 23/53] add language dropdown (#4716) --- docs/css/language-dropdown.css | 57 +++++++++++++++++++++++++ docs/language-dropdown.js | 77 ++++++++++++++++++++++++++++++++++ 2 files changed, 134 insertions(+) create mode 100644 docs/css/language-dropdown.css create mode 100644 docs/language-dropdown.js diff --git a/docs/css/language-dropdown.css b/docs/css/language-dropdown.css new file mode 100644 index 000000000..0eb810545 --- /dev/null +++ b/docs/css/language-dropdown.css @@ -0,0 +1,57 @@ +/* Language dropdown: injected by language-dropdown.js into the sidebar + footer, to the right of Mintlify's theme selector. Mirrors the almond + theme pill's exact metrics (lg:h-7 desktop / 2.375rem mobile, rounded-full, + border-gray-200/70, dark:border-white/[0.07]) so the two controls read as + one family. */ +#language-switch { + margin-left: auto; + display: inline-flex; + align-items: center; +} + +#language-switch select { + appearance: none; + -webkit-appearance: none; + background-color: transparent; + border: 1px solid rgb(229 231 235 / 0.7); + border-radius: 9999px; + color: rgb(107 114 128); + cursor: pointer; + font-size: 0.75rem; + line-height: 1rem; + height: 2.375rem; + padding: 0 1.375rem 0 0.75rem; + /* Chevron, drawn in the same gray as the label text. */ + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%236b7280' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: right 0.5rem center; + background-size: 0.7rem; + transition: border-color 0.2s; +} + +@media (min-width: 1024px) { + #language-switch select { + height: 1.75rem; + } +} + +#language-switch select:hover { + color: rgb(75 85 99); + border-color: rgb(229 231 235); +} + +#language-switch select:focus-visible { + outline: 2px solid rgb(45 0 247 / 0.4); + outline-offset: 1px; +} + +.dark #language-switch select { + border-color: rgb(255 255 255 / 0.07); + color: rgb(156 163 175); + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%239ca3af' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E"); +} + +.dark #language-switch select:hover { + color: rgb(209 213 219); + border-color: rgb(255 255 255 / 0.1); +} diff --git a/docs/language-dropdown.js b/docs/language-dropdown.js new file mode 100644 index 000000000..4eb5131f6 --- /dev/null +++ b/docs/language-dropdown.js @@ -0,0 +1,77 @@ +// Language dropdown: a small Python/TypeScript switcher injected into the +// sidebar footer, next to Mintlify's theme selector. Selecting the other +// language navigates to that project's docs site; selecting the current +// language is a no-op. Styling lives in css/language-dropdown.css. +(function () { + if (typeof window === "undefined") return; + + var CURRENT_LANGUAGE = "python"; + + // TODO: fastmcp-ts has no public docs site URL discoverable in either repo + // yet. Until it exists, point at the repo README (the same cross-link the + // welcome page uses), then replace with the real docs URL. + var TYPESCRIPT_DOCS_URL = "https://github.com/PrefectHQ/fastmcp-ts"; + var PYTHON_DOCS_URL = "https://gofastmcp.com"; + + var URLS = { python: PYTHON_DOCS_URL, typescript: TYPESCRIPT_DOCS_URL }; + + function findThemeSelector() { + // Mintlify's sidebar-footer DOM is not a stable public API, so probe a + // few markers (almond theme first) and give up quietly if none match. + return ( + document.querySelector("[data-theme-preference-switch]") || + document.querySelector('[role="group"][aria-label="Theme preference"]') + ); + } + + function buildDropdown() { + var label = document.createElement("label"); + label.id = "language-switch"; + + var select = document.createElement("select"); + select.setAttribute("aria-label", "Switch documentation language"); + + [ + ["python", "Python"], + ["typescript", "TypeScript"], + ].forEach(function (entry) { + var option = document.createElement("option"); + option.value = entry[0]; + option.textContent = entry[1]; + if (entry[0] === CURRENT_LANGUAGE) option.selected = true; + select.appendChild(option); + }); + + select.addEventListener("change", function () { + if (select.value === CURRENT_LANGUAGE) return; + window.location.href = URLS[select.value]; + }); + + label.appendChild(select); + return label; + } + + function addDropdown() { + if (document.getElementById("language-switch")) return; + var theme = findThemeSelector(); + if (!theme || !theme.parentElement) return; + // Insert after the theme pill; margin-left:auto floats it right. + theme.parentElement.insertBefore(buildDropdown(), theme.nextSibling); + } + + function run() { + if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", addDropdown); + } else { + addDropdown(); + } + } + + run(); + + // Mintlify re-renders the sidebar on client-side navigation; re-inject when + // the dropdown disappears. + new MutationObserver(function () { + if (!document.getElementById("language-switch")) addDropdown(); + }).observe(document.body, { subtree: true, childList: true }); +})(); From bcef61d8064a145a2f32416ec969a351f70bcbe8 Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Thu, 30 Jul 2026 10:18:03 -0400 Subject: [PATCH 24/53] Route published docs through pull requests (#4713) --- .github/workflows/publish-fastmcp.yml | 38 +++++++++++++++++++++++---- CLAUDE.md | 4 ++- docs/development/releases.mdx | 2 +- 3 files changed, 37 insertions(+), 7 deletions(-) diff --git a/.github/workflows/publish-fastmcp.yml b/.github/workflows/publish-fastmcp.yml index dc32179cb..8b2ce33b2 100644 --- a/.github/workflows/publish-fastmcp.yml +++ b/.github/workflows/publish-fastmcp.yml @@ -178,19 +178,27 @@ jobs: run: uv publish -v dist/fastmcp-*.tar.gz dist/fastmcp-*.whl update-published-docs: - name: Update published-docs branch + name: Open published-docs PR runs-on: ubuntu-latest needs: pypi-publish if: github.event_name == 'workflow_run' && github.event.workflow_run.event == 'release' && needs['pypi-publish'].outputs.is_prerelease != 'true' - timeout-minutes: 2 + timeout-minutes: 5 permissions: - contents: write + contents: read steps: + - name: Generate Marvin App token + id: marvin-token + uses: actions/create-github-app-token@v3 + with: + app-id: ${{ secrets.MARVIN_APP_ID }} + private-key: ${{ secrets.MARVIN_APP_PRIVATE_KEY }} + - uses: actions/checkout@v7 with: fetch-depth: 0 ref: ${{ github.event.workflow_run.head_sha }} + token: ${{ steps.marvin-token.outputs.token }} - name: Check release line id: release_line @@ -205,6 +213,26 @@ jobs: echo "Release commit is not on ${DEFAULT_BRANCH}; skipping published-docs update." fi - - name: Point published-docs at published release + - name: Prepare published docs tree if: steps.release_line.outputs.update_published_docs == 'true' - run: git push --force origin "HEAD:published-docs" + env: + RELEASE_SHA: ${{ github.event.workflow_run.head_sha }} + run: | + git fetch origin published-docs + git switch --force-create published-docs-sync origin/published-docs + git read-tree --reset -u "$RELEASE_SHA" + test "$(git write-tree)" = "$(git rev-parse "${RELEASE_SHA}^{tree}")" + + - name: Open published docs PR + if: steps.release_line.outputs.update_published_docs == 'true' + uses: peter-evans/create-pull-request@v8 + with: + token: ${{ steps.marvin-token.outputs.token }} + base: published-docs + branch: marvin/publish-docs-v${{ needs.pypi-publish.outputs.version }} + commit-message: "Publish FastMCP v${{ needs.pypi-publish.outputs.version }} docs" + title: "Publish FastMCP v${{ needs.pypi-publish.outputs.version }} docs" + body: "Updates `published-docs` to the exact release tree. Merging publishes the documentation to production." + delete-branch: true + author: "marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com>" + committer: "marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com>" diff --git a/CLAUDE.md b/CLAUDE.md index 5a4ef8bed..1a2cbddb0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -117,7 +117,9 @@ Set `target_commitish` to the same branch that will receive the release tag. For **Patch releases** (3.1.1, 3.0.2) get 1-2 sentences explaining what broke and what the fix does. Keep it minimal — the auto-generated changelog has the details. -**Merge the docs changelog PR *before* cutting the release, not after.** The post-publish `update-published-docs` job force-pushes the `published-docs` branch (which gofastmcp.com serves) to the released commit for stable releases on the default branch, so the changelog entry only reaches the live site if it's already in the commit being tagged. Land the docs PR on the release target branch first, then cut the release from that branch. If you tag first and merge docs after, this release's changelog won't appear on the live site until the next default-branch stable release force-pushes `published-docs` forward. Maintenance releases from `release/3.x` or `release/2.x` publish packages and GitHub notes without repointing `published-docs`; add their changelog entries on the maintenance branch, slotted into the matching major-version section. Two hand-maintained files mirror the GitHub release and must get a new entry for every version, newest at the top (these are `.mdx` and are not covered by the prek Prettier hook, which only runs on `yaml`/`json5` — match the existing entries' style by hand): +**Publish docs through a PR.** The `published-docs` branch serves gofastmcp.com, and repository rules reject direct pushes and force-pushes to it. Stable releases from `main` automatically open a publication PR after PyPI succeeds. For prereleases and later docs follow-ups, create the same PR manually: start a temporary branch from the current `published-docs`, make a single commit whose tree exactly matches the desired commit on `main`, and use `published-docs` as the PR base. Merging publishes to production. Never push directly to `published-docs`. + +**Merge the docs changelog PR *before* cutting the release, not after.** The post-publish `update-published-docs` job opens a PR that syncs `published-docs` to the released commit for stable releases on the default branch, so the changelog entry only reaches the live site if it's already in the commit being tagged. Land the docs PR on the release target branch first, then cut the release from that branch. If you tag first and merge docs after, this release's publication PR will not include the changelog; publish `main` manually through the PR flow above or wait for the next default-branch stable release. Maintenance releases from `release/3.x` or `release/2.x` publish packages and GitHub notes without repointing `published-docs`; add their changelog entries on the maintenance branch, slotted into the matching major-version section. Two hand-maintained files mirror the GitHub release and must get a new entry for every version, newest at the top (these are `.mdx` and are not covered by the prek Prettier hook, which only runs on `yaml`/`json5` — match the existing entries' style by hand): - `docs/changelog.mdx` is the full mirror. Add an `` block with: a bold linked title (`**[v: ]()**`), a condensed 1-paragraph intro (one sentence for patches), the full categorized PR list reformatted from the `--generate-notes` output (`* by [@user](https://github.com/user) in [#NNNN](<pull-url>)`), a `## New Contributors` list (plain `@user`, linked PR), and a `**Full Changelog**: [vA...vB](<compare-url>)` line. - `docs/updates.mdx` is the skimmable card feed. Add an `<Update label="FastMCP <version>" description="Month DD, YYYY" tags={["Releases"]}>` wrapping a `<Card>` that links to the GitHub release, with a 1-2 sentence summary and (for point releases) a handful of emoji-bulleted highlights. diff --git a/docs/development/releases.mdx b/docs/development/releases.mdx index ecba9a25f..f537703e2 100644 --- a/docs/development/releases.mdx +++ b/docs/development/releases.mdx @@ -65,7 +65,7 @@ Our release process is intentionally simple: 2. Generate release notes automatically, and curate or add additional editorial information as needed 3. GitHub releases automatically trigger PyPI deployments -Current-major releases target `main`. Maintenance releases target their release branch, such as `release/3.x` for 3.x patches and `release/2.x` for 2.x patches. Stable releases from `main` update the `published-docs` branch after PyPI publishing succeeds; maintenance releases publish packages and GitHub release notes without repointing the live docs branch. +Current-major releases target `main`. Maintenance releases target their release branch, such as `release/3.x` for 3.x patches and `release/2.x` for 2.x patches. Stable releases from `main` open a PR that syncs the release commit to `published-docs` after PyPI publishing succeeds; merging that PR publishes the live docs. Prereleases skip the automatic PR and use the same PR-based sync when their docs are ready to publish. Maintenance releases publish packages and GitHub release notes without repointing the live docs branch. This automation lets maintainers focus on code quality rather than release mechanics. From bc07264529fe108b43ae81d116a15d3e2808d23b Mon Sep 17 00:00:00 2001 From: Jake Kaplan <40362401+jakekaplan@users.noreply.github.com> Date: Thu, 30 Jul 2026 16:23:33 -0400 Subject: [PATCH 25/53] Fix self-referential connection error causes (#4720) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with OpenAI Codex --- fastmcp_slim/fastmcp/client/client.py | 5 ++- tests/client/auth/test_oauth_client.py | 4 +- tests/client/client/test_session.py | 55 ++++++++++++++++++++++++-- 3 files changed, 59 insertions(+), 5 deletions(-) diff --git a/fastmcp_slim/fastmcp/client/client.py b/fastmcp_slim/fastmcp/client/client.py index c9b9aa1eb..55aff6408 100644 --- a/fastmcp_slim/fastmcp/client/client.py +++ b/fastmcp_slim/fastmcp/client/client.py @@ -1022,7 +1022,10 @@ class Client( raise RuntimeError( "Session task completed without exception but connection failed" ) - raise _connection_failure(exception) from exception + failure = _connection_failure(exception) + if failure is exception: + raise exception + raise failure from exception self._session_state.nesting_counter += 1 diff --git a/tests/client/auth/test_oauth_client.py b/tests/client/auth/test_oauth_client.py index 1ded252f8..22ca63af0 100644 --- a/tests/client/auth/test_oauth_client.py +++ b/tests/client/auth/test_oauth_client.py @@ -96,10 +96,12 @@ async def test_unauthorized(client_unauthorized: Client): SDK v2 surfaces the server's 401 as an MCPError ("Server returned an error response") rather than re-raising the raw httpx2.HTTPStatusError. """ - with pytest.raises(MCPError, match="error response"): + with pytest.raises(MCPError, match="error response") as exc_info: async with client_unauthorized: pass + assert exc_info.value.__cause__ is not exc_info.value + async def test_ping(streamable_http_server: str): """Test that we can ping the server. diff --git a/tests/client/client/test_session.py b/tests/client/client/test_session.py index 0579aa6d5..116bff69c 100644 --- a/tests/client/client/test_session.py +++ b/tests/client/client/test_session.py @@ -1,18 +1,33 @@ """Client session and task error propagation tests.""" import asyncio +from collections.abc import AsyncIterator from contextlib import asynccontextmanager +from typing import Any +import httpx2 import pytest -from mcp import ClientSession -from mcp_types import TextContent +from mcp import ClientSession, MCPError +from mcp_types import INTERNAL_ERROR, TextContent from fastmcp import FastMCP from fastmcp.client import Client -from fastmcp.client.transports import PythonStdioTransport +from fastmcp.client.transports import ClientTransport, PythonStdioTransport from fastmcp.client.transports.base import TransportOptions +class _FailingTransport(ClientTransport): + def __init__(self, exception: Exception) -> None: + self._exception = exception + + @asynccontextmanager + async def connect_session( + self, **session_kwargs: Any + ) -> AsyncIterator[ClientSession]: + raise self._exception + yield + + class TestSessionTaskErrorPropagation: """Tests for ensuring session task errors propagate to client calls. @@ -143,6 +158,40 @@ class TestSessionTaskErrorPropagation: client._session_state.session_task = original_task +class TestConnectionFailurePropagation: + @pytest.mark.parametrize( + "failure", + [ + MCPError(code=INTERNAL_ERROR, message="upstream failed"), + httpx2.HTTPStatusError( + "upstream unavailable", + request=httpx2.Request("GET", "https://example.com"), + response=httpx2.Response(503), + ), + ], + ids=["mcp-error", "http-status-error"], + ) + async def test_preserves_passthrough_exception(self, failure: Exception): + client = Client(transport=_FailingTransport(failure)) + + with pytest.raises(type(failure)) as exc_info: + async with client: + pass + + assert exc_info.value is failure + assert exc_info.value.__cause__ is not failure + + async def test_wraps_other_failures_with_cause(self): + failure = OSError("connection refused") + client = Client(transport=_FailingTransport(failure)) + + with pytest.raises(RuntimeError, match="Client failed to connect") as exc_info: + async with client: + pass + + assert exc_info.value.__cause__ is failure + + class TestCustomSessionClass: """Transports build the session class the client asks for.""" From 40c3e122e8e5cce4d080c3b87805ea7bae92c96c Mon Sep 17 00:00:00 2001 From: nate nowack <thrast36@gmail.com> Date: Sun, 2 Aug 2026 08:33:32 -0500 Subject: [PATCH 26/53] Write downloaded skill text as UTF-8 (#4715) --- fastmcp_slim/fastmcp/utilities/skills.py | 2 +- tests/utilities/test_skills.py | 47 ++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/fastmcp_slim/fastmcp/utilities/skills.py b/fastmcp_slim/fastmcp/utilities/skills.py index 2c93b1f7f..73b859548 100644 --- a/fastmcp_slim/fastmcp/utilities/skills.py +++ b/fastmcp_slim/fastmcp/utilities/skills.py @@ -205,7 +205,7 @@ async def download_skill( # Write content if isinstance(content, mcp_types.TextResourceContents): - file_path.write_text(content.text) + file_path.write_text(content.text, encoding="utf-8") elif isinstance(content, mcp_types.BlobResourceContents): file_path.write_bytes(base64.b64decode(content.blob)) else: diff --git a/tests/utilities/test_skills.py b/tests/utilities/test_skills.py index 62268ac91..90c6f5726 100644 --- a/tests/utilities/test_skills.py +++ b/tests/utilities/test_skills.py @@ -269,6 +269,53 @@ class TestDownloadSkill: downloaded = (result / "SKILL.md").read_text() assert downloaded == original + async def test_writes_text_resources_as_utf8( + self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch + ): + content = "Use the tool — then stop." + manifest = { + "skill": "unicode", + "files": [ + { + "path": "SKILL.md", + "size": len(content.encode("utf-8")), + "hash": "sha256:unicode", + } + ], + } + client = FakeResourceReader( + { + "skill://unicode/_manifest": [ + text_resource("skill://unicode/_manifest", json.dumps(manifest)) + ], + "skill://unicode/SKILL.md": [ + text_resource("skill://unicode/SKILL.md", content) + ], + } + ) + original_write_text = Path.write_text + + def locale_sensitive_write_text( + path: Path, + data: str, + encoding: str | None = None, + errors: str | None = None, + newline: str | None = None, + ) -> int: + return original_write_text( + path, + data, + encoding=encoding or "ascii", + errors=errors, + newline=newline, + ) + + monkeypatch.setattr(Path, "write_text", locale_sensitive_write_text) + + result = await download_skill(cast(Client, client), "unicode", tmp_path) + + assert (result / "SKILL.md").read_text(encoding="utf-8") == content + async def test_raises_if_exists_without_overwrite( self, skills_server: FastMCP, tmp_path: Path ): From 9034a2eb4bdcbccbad5851d6d98ceae445d3f5c4 Mon Sep 17 00:00:00 2001 From: YAO_001 <yaoyaoguonan@outlook.com> Date: Sun, 2 Aug 2026 21:41:36 +0800 Subject: [PATCH 27/53] Fix CodeMode tool error propagation (#4704) Co-authored-by: nate nowack <thrast36@gmail.com> --- fastmcp_slim/pyproject.toml | 2 +- .../experimental/transforms/test_code_mode.py | 57 ++++++++ uv.lock | 126 +++++++++--------- 3 files changed, 121 insertions(+), 64 deletions(-) diff --git a/fastmcp_slim/pyproject.toml b/fastmcp_slim/pyproject.toml index ba408404d..b009efd42 100644 --- a/fastmcp_slim/pyproject.toml +++ b/fastmcp_slim/pyproject.toml @@ -74,7 +74,7 @@ client = [ "authlib>=1.6.11", "py-key-value-aio[filetree,keyring,memory]>=0.4.4,<0.5.0", ] -code-mode = ["pydantic-monty==0.0.17"] +code-mode = ["pydantic-monty==0.0.18"] gemini = ["google-genai>=1.18.0", "jsonref>=1.1.0"] mcp = [ "exceptiongroup>=1.2.2", diff --git a/tests/experimental/transforms/test_code_mode.py b/tests/experimental/transforms/test_code_mode.py index 557f33c8b..cea34de50 100644 --- a/tests/experimental/transforms/test_code_mode.py +++ b/tests/experimental/transforms/test_code_mode.py @@ -791,6 +791,63 @@ async def test_code_mode_monty_execute_chaining() -> None: assert _unwrap_result(result) == {"result": 13} +@requires_monty +@pytest.mark.parametrize( + ("failing_call", "expected_message"), + [ + ("await call_tool('no_such_tool', {})", "Unknown tool: no_such_tool"), + ("await call_tool('boom', {})", "deliberate tool failure"), + ], + ids=["unknown-tool", "tool-error"], +) +async def test_code_mode_monty_call_tool_errors_are_catchable( + failing_call: str, expected_message: str +) -> None: + """Sandbox code can catch call_tool errors and preserve prior work.""" + mcp = FastMCP("CodeMode Monty Catch Errors") + + @mcp.tool + def add(x: int, y: int) -> int: + return x + y + + @mcp.tool + def boom() -> None: + raise ToolError("deliberate tool failure") + + mcp.add_transform(CodeMode(sandbox_provider=MontySandboxProvider())) + + code = ( + "total = (await call_tool('add', {'x': 2, 'y': 3}))['result']\n" + "caught = None\n" + "try:\n" + f" {failing_call}\n" + "except Exception as exc:\n" + " caught = str(exc)\n" + "return {'caught': caught, 'total': total}" + ) + result = await _run_tool(mcp, "execute", {"code": code}) + + assert _unwrap_result(result) == { + "caught": expected_message, + "total": 5, + } + + +@requires_monty +async def test_code_mode_monty_uncaught_call_tool_error_surfaces() -> None: + """Uncaught backend errors still propagate out of the sandbox.""" + mcp = FastMCP("CodeMode Monty Uncaught Error") + + @mcp.tool + def boom() -> None: + raise ToolError("deliberate tool failure") + + mcp.add_transform(CodeMode(sandbox_provider=MontySandboxProvider())) + + with pytest.raises(ToolError, match="deliberate tool failure"): + await _run_tool(mcp, "execute", {"code": "return await call_tool('boom', {})"}) + + @requires_monty async def test_code_mode_monty_bare_call_returns_empty() -> None: """Pins the reported #4263 symptom as a usage error, not a sandbox bug. diff --git a/uv.lock b/uv.lock index a169584d7..727bf1183 100644 --- a/uv.lock +++ b/uv.lock @@ -1065,7 +1065,7 @@ requires-dist = [ { name = "py-key-value-aio", extras = ["filetree", "keyring", "memory"], marker = "extra == 'client'", specifier = ">=0.4.4,<0.5.0" }, { name = "py-key-value-aio", extras = ["filetree", "keyring", "memory"], marker = "extra == 'server'", specifier = ">=0.4.4,<0.5.0" }, { name = "pydantic", extras = ["email"], specifier = ">=2.12.0" }, - { name = "pydantic-monty", marker = "extra == 'code-mode'", specifier = "==0.0.17" }, + { name = "pydantic-monty", marker = "extra == 'code-mode'", specifier = "==0.0.18" }, { name = "pydantic-settings", specifier = ">=2.0.0" }, { name = "pyjwt", marker = "extra == 'azure'", specifier = ">=2.12.0" }, { name = "pyperclip", marker = "extra == 'server'", specifier = ">=1.9.0" }, @@ -2280,73 +2280,73 @@ wheels = [ [[package]] name = "pydantic-monty" -version = "0.0.17" +version = "0.0.18" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f4/f8/431ba0b79d02922811392c4e3d283d6508f7052ceaa1936cc34878703ecc/pydantic_monty-0.0.17.tar.gz", hash = "sha256:9c4904a8fbc63282793f3afd2d180124494c7fc371783f365e5691c9586360af", size = 1007724, upload-time = "2026-04-22T20:13:48.915Z" } +sdist = { url = "https://files.pythonhosted.org/packages/14/5b/bb6a8bfdf13eb9808c966bdac064a40ce9ac881ec6d64dba3e055888f22b/pydantic_monty-0.0.18.tar.gz", hash = "sha256:c43794c7c4664fa1403d4841459d0e23f01b4f552283db638f5b40ced4dac6a1", size = 1197105, upload-time = "2026-05-29T08:31:41.077Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5e/19/8105bc0b3acb42f6cb48a29669a5e21316bc05e3e9b6fab64cf94b483712/pydantic_monty-0.0.17-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3c3b6c026d8a0437eeb4d6b2d908be75e2715e0555b9a13f076b7e9ba9bbae19", size = 7344730, upload-time = "2026-04-22T20:13:24.408Z" }, - { url = "https://files.pythonhosted.org/packages/6d/a2/7281cdb37481c4252292b63bebf737c87d0fd463f3174499608607de0907/pydantic_monty-0.0.17-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c80b4d34437abd209c042f81f8ecea81a097022fb9b01431ab859b877edfbc4d", size = 7334937, upload-time = "2026-04-22T20:15:06.923Z" }, - { url = "https://files.pythonhosted.org/packages/a5/68/0bf7c0c627a56d8653b42888a3c1fc33cd33d2532ec456d9358275d7c792/pydantic_monty-0.0.17-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:beecc1f7e5b10db40d7b2b24a68166a36514289a2402bfef370a7984e90a2ab8", size = 7864543, upload-time = "2026-04-22T20:14:46.273Z" }, - { url = "https://files.pythonhosted.org/packages/09/9b/5a6f006541fd3bdc64b6dfbbaeabfb2244c89a22d7077a1fc92ec497c03e/pydantic_monty-0.0.17-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:64ea7babdcc9fba93089fa52589b6d0549f755e37500f6cf4aeaeb8e56328a3e", size = 7138764, upload-time = "2026-04-22T20:15:30.516Z" }, - { url = "https://files.pythonhosted.org/packages/01/cc/59cca979bd427d166df8c827fba9e794c4a5c08943e225a22adf9854a78f/pydantic_monty-0.0.17-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a7fe77a191205becb622eaecb075e8bcbbbe4dac20a916d9c58ce6d59a22a8da", size = 7444006, upload-time = "2026-04-22T20:15:23.386Z" }, - { url = "https://files.pythonhosted.org/packages/1f/c5/d027170fb33fcbc038febb76dfd2d9047f5194a250ea608e3ed8e5ec28d4/pydantic_monty-0.0.17-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2cdbefc180cc83c8b8415aaf95b9099bb2cb15261f40ebe2c92f13e7d52439a4", size = 7967564, upload-time = "2026-04-22T20:14:57.315Z" }, - { url = "https://files.pythonhosted.org/packages/3e/01/ac0d4bc1ff00acfac14b7cb2ee322d08778c206cd57f43da8206a2f6ce78/pydantic_monty-0.0.17-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:575ce5aa31db18bbbf6275f00e9b0c005ca393bfb73a2f306a577ad490ec2d98", size = 8199021, upload-time = "2026-04-22T20:15:14.488Z" }, - { url = "https://files.pythonhosted.org/packages/51/85/8d0c6e5f127da9ebc0fcda6e411592d12b7606347d67aecd4363df5eed6b/pydantic_monty-0.0.17-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e252ec54fc4728406045f7be36ca45dbea8e6856df9c6154b1b9821b8952dfa2", size = 7769814, upload-time = "2026-04-22T20:14:55.197Z" }, - { url = "https://files.pythonhosted.org/packages/ac/cc/cb4d1b14b039eab00b33a7274f15f81739c3f272e2dfbeb8fb13c6b0c85d/pydantic_monty-0.0.17-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:fba71e5cb49f15a1446ecee142c8cc11f4bd6df4fcb4926465c83181474b2fd4", size = 7317432, upload-time = "2026-04-22T20:14:19.993Z" }, - { url = "https://files.pythonhosted.org/packages/c8/16/737c7a023abbcb21848eb4d58f7167d9f4f8cdc46858ce8ed835cc2c137c/pydantic_monty-0.0.17-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:69136647abd56f804987834e37573adcc5c3b3d05013b8b3a2939f44b3bd5199", size = 7767816, upload-time = "2026-04-22T20:13:40.002Z" }, - { url = "https://files.pythonhosted.org/packages/dc/9f/5302b784f882ae8a8396f29f8c5ab4c16524c173a3d777b94af33858fdf2/pydantic_monty-0.0.17-cp310-cp310-win32.whl", hash = "sha256:d5b3beb6169b59adea10fdefb1e54bfa9a66165404891dfb6fcf16f7749cda3b", size = 7230648, upload-time = "2026-04-22T20:14:27.03Z" }, - { url = "https://files.pythonhosted.org/packages/1c/27/8c219f619dad466ec25db365acf88e2a50450dd862e0daff0eb281b6176b/pydantic_monty-0.0.17-cp310-cp310-win_amd64.whl", hash = "sha256:50ed9561b6dd1a1863d4cac81e4eaca64cb10ab541aaab92fcb5996739bb8e7f", size = 8075073, upload-time = "2026-04-22T20:14:17.073Z" }, - { url = "https://files.pythonhosted.org/packages/e7/42/ca8e42d9f3318f5c454cf8b168d814ec97c6f2afc38756d4b1b806184f6d/pydantic_monty-0.0.17-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:af890d691f6055491a4e643dd5bf09e07bd7a20ad70038531aada6415ab8794a", size = 7344138, upload-time = "2026-04-22T20:13:29.155Z" }, - { url = "https://files.pythonhosted.org/packages/56/c8/cfaf0a56087301d4e88f72cf54ea45a7eebc09c021c85b8864447f1e3755/pydantic_monty-0.0.17-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2f38a69858dfdd2c9474156616d05e25a288e2080aee24152fa40c19ad425f0e", size = 7334903, upload-time = "2026-04-22T20:14:31.489Z" }, - { url = "https://files.pythonhosted.org/packages/51/77/a751a6f73f854aa85fed94cfa5ecab21d7bf218c9fa03c96f9edf470cc4e/pydantic_monty-0.0.17-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:bb88264e291cee56770a775f57125538c4713c6d362e89ee63bff506f650a0df", size = 7864258, upload-time = "2026-04-22T20:13:15.594Z" }, - { url = "https://files.pythonhosted.org/packages/0a/fe/2eb51eb37e9f712cada64fa8d7df4b63b1f5fc635290147ab158ff0e1ef1/pydantic_monty-0.0.17-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54c317611454aba8be7ca96aeeea9429f4702a5c4ba89812bea82bed0d8e34fd", size = 7138153, upload-time = "2026-04-22T20:14:22.255Z" }, - { url = "https://files.pythonhosted.org/packages/bb/15/835b10cdec3b96b089eef9899df6850b7f84a10225c491698b0ecf8e532a/pydantic_monty-0.0.17-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9563b5b4933f0f08c0e66ec66aaa4f43f2388bcc04b984e58aab2146dacd3829", size = 7443572, upload-time = "2026-04-22T20:13:17.951Z" }, - { url = "https://files.pythonhosted.org/packages/16/92/aca140923fad8a2821a135cfeaa2fbb3321063bbadaa760424a016bb1ac6/pydantic_monty-0.0.17-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:35f267a501bc1910178a1515fdd3dd927273fbb44e44b8718cb3b33aee79f41b", size = 7967178, upload-time = "2026-04-22T20:14:06.032Z" }, - { url = "https://files.pythonhosted.org/packages/d7/ca/7c4ff1e3fe2e82a4745decfca67b54a7a61cd306875e32d8e41c5192c69e/pydantic_monty-0.0.17-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:35b000c52755f25f322ea7c4d079f09aa60635ffe24a6463899e423066a41bf3", size = 8198241, upload-time = "2026-04-22T20:15:21.2Z" }, - { url = "https://files.pythonhosted.org/packages/30/0b/702db7b753b96ebc6713e7cbdfaecdb471df3e3cb0f0f6e828620a743b78/pydantic_monty-0.0.17-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61b517776ad13aa4580b1dd89188b18296ceeaf88256423563bbc99e804fd83f", size = 7768859, upload-time = "2026-04-22T20:13:20.044Z" }, - { url = "https://files.pythonhosted.org/packages/29/9c/8d16e0cc0c36d1444f25d57da68dd22216bf0961c457a482429cec32141b/pydantic_monty-0.0.17-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:5da5362ef25665a23a3b13024497719f65cafa61d696cac76429f84701bee2e2", size = 7316674, upload-time = "2026-04-22T20:14:52.579Z" }, - { url = "https://files.pythonhosted.org/packages/6f/4d/d47ae703d402e45475333c4bf11b117c8068305f00c1363dbaea13d0fd09/pydantic_monty-0.0.17-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7e655b6ddd552c02b751f1d57fc291fbd5654ff8b166a8bd634857879160d0b7", size = 7767515, upload-time = "2026-04-22T20:15:16.539Z" }, - { url = "https://files.pythonhosted.org/packages/99/9b/e17fb50d0df5cf9908f8fffa25c5909ed0eb92ca102ded06f7a6d6133e78/pydantic_monty-0.0.17-cp311-cp311-win32.whl", hash = "sha256:ea8b3ae8c42d572cefad841d3bda63cc458d9de2361cb9172914250e6dbe2c75", size = 7230347, upload-time = "2026-04-22T20:14:08.083Z" }, - { url = "https://files.pythonhosted.org/packages/5e/82/d3119f59652d04bcf69d671ddbd38464d5775fbc738a258d3c8f7800e29d/pydantic_monty-0.0.17-cp311-cp311-win_amd64.whl", hash = "sha256:3293c2f7524bfc7c3d8c794f1c1dc1eb4cf9c65a5e222061e2218ced85f3f6df", size = 8074183, upload-time = "2026-04-22T20:14:50.42Z" }, - { url = "https://files.pythonhosted.org/packages/d1/31/95827babdb35149f076c5d191b6b1e7a7c58f4bc72432f905e02e4e3231e/pydantic_monty-0.0.17-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:27c2254fa7a7b05e969f79578889230d293c62e0b1ee28371ec4f3c54b14426a", size = 7342248, upload-time = "2026-04-22T20:15:18.775Z" }, - { url = "https://files.pythonhosted.org/packages/cb/67/ca9cfc07cd445d22def53e9db86912f9ae3e11ef772ce41c2ff41a47eac5/pydantic_monty-0.0.17-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:445cc471ce6f5a88ef06741b7ebc7002a2253d182f55a2f47094d4adedaaf497", size = 7311255, upload-time = "2026-04-22T20:15:27.913Z" }, - { url = "https://files.pythonhosted.org/packages/df/96/abc9c4972d91a9673435b84e12b99d038e42d1f99648fc9e5f242e09d00e/pydantic_monty-0.0.17-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:39121038405911f59da7bf61164251f59bad3fb1b0cd28f43c42c3949eee2c8a", size = 7868109, upload-time = "2026-04-22T20:15:04.779Z" }, - { url = "https://files.pythonhosted.org/packages/75/82/9e4d55529bb99d882b9277a721762537a8bc1345ab1d052bb614a88bd15b/pydantic_monty-0.0.17-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dafc8ffe57c257002f623afdb7d0e41f73de850179ebd90b42611e4f2b6f9884", size = 7139709, upload-time = "2026-04-22T20:15:25.386Z" }, - { url = "https://files.pythonhosted.org/packages/96/97/f1af6acefb7bb38d73934d6853998bbd327de7418b811372519080d9fd84/pydantic_monty-0.0.17-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ea00838ef8f37dcd8085defcbdfe89fdd05297a6533f1d3f4cad857d13cedc7b", size = 7450444, upload-time = "2026-04-22T20:13:37.974Z" }, - { url = "https://files.pythonhosted.org/packages/1d/91/af92ef409e1c065345cf1451bbcf19e00f70a250b8372ec65143ca9a9238/pydantic_monty-0.0.17-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:683d18089acf14d0de293245b9e37c7f0ec64e6d266f6773144211931aa3ec97", size = 7967525, upload-time = "2026-04-22T20:13:42.674Z" }, - { url = "https://files.pythonhosted.org/packages/7b/1f/23ecd6e268ef24ce6b0fe4a1e76a314990d2e923ac5791e29d657418243d/pydantic_monty-0.0.17-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c1829993dd50cf497cbed66ea9f6c8ff7d157d22592a05c7399f92fb8a549e3c", size = 8199124, upload-time = "2026-04-22T20:15:00.02Z" }, - { url = "https://files.pythonhosted.org/packages/42/2a/36b694ea0c7e202250a81a57faf00f218738da6c5d070c752f2d81cd34ce/pydantic_monty-0.0.17-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a7869e3f41a54cc588096c52a8a4de25ecd81e75c867ea4164b14ea1ae1a57f", size = 7739623, upload-time = "2026-04-22T20:14:37.57Z" }, - { url = "https://files.pythonhosted.org/packages/98/e5/090357d7bc0f0751d1afbb71330695fa26554699c88ba56ecaad91657088/pydantic_monty-0.0.17-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:f9c17663e2c6f07aec5bc54cd7e39a9e20f250a97a9081e1b2b932eb00d0afc5", size = 7317755, upload-time = "2026-04-22T20:14:48.367Z" }, - { url = "https://files.pythonhosted.org/packages/15/63/67200070cf33325ecfda81d4aee3bf312250ce80bd73058103e04e0f3587/pydantic_monty-0.0.17-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5d2cf98afe2fb124f6ade91d9663d54277478cad417164f83df1854a41ef450c", size = 7769158, upload-time = "2026-04-22T20:14:39.611Z" }, - { url = "https://files.pythonhosted.org/packages/58/ce/9ecfbc2f45406cfb247fafdea4f4a8412db3e559a22c4385eb15266ba2c1/pydantic_monty-0.0.17-cp312-cp312-win32.whl", hash = "sha256:b2185cc4effbbd6793eed4e0f0bcb6a3dbfbb3289ea4d47888708813f0a3dd47", size = 7227917, upload-time = "2026-04-22T20:14:15.122Z" }, - { url = "https://files.pythonhosted.org/packages/d3/80/9be3bef8273817ccc17da25c3ce4ff5d5d45e5629c17eacc90cdef073821/pydantic_monty-0.0.17-cp312-cp312-win_amd64.whl", hash = "sha256:7833daed757ec9b09b627cc3577a4a76b114c5148f779531d7cfdb1095bcf0a9", size = 8043469, upload-time = "2026-04-22T20:13:22.102Z" }, - { url = "https://files.pythonhosted.org/packages/b5/44/0e106b8b27eb93b66e4f3d279486464e05ba5ee31088848e58b5f506f879/pydantic_monty-0.0.17-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:e0cdac8c3c16477596bc96ee1cec4f2fbaccd089e2daa1e7b9f227cc89f97cb1", size = 7341507, upload-time = "2026-04-22T20:14:41.717Z" }, - { url = "https://files.pythonhosted.org/packages/e5/88/a0315fa08e62e2d1ef00c03d8202d7bef3f1f71543bebfb916fea265c0a2/pydantic_monty-0.0.17-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:37290d6a1c35aba5cfb8b490bb31c0d822e8ddca8f3ca9ea068e30930d80dd1e", size = 7311916, upload-time = "2026-04-22T20:13:34.022Z" }, - { url = "https://files.pythonhosted.org/packages/e7/e5/e4da6acb408594cbfbfb8dd3c0491b9b2ee54e9183e7ebc5f584baa07af9/pydantic_monty-0.0.17-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:49f252b2fb918686d3e8f76cb30245e782d1560a7fa68dbc0f6940d83c12bd41", size = 7867465, upload-time = "2026-04-22T20:13:31.466Z" }, - { url = "https://files.pythonhosted.org/packages/58/d4/64c2f8eb708a743b0944ea8f71dfd51bc655285b4be28d55577dafbb29fa/pydantic_monty-0.0.17-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2254d25c34463d67069f5f1567157bde9311654cd5371f8933b8ea9815bfa26a", size = 7139262, upload-time = "2026-04-22T20:14:10.715Z" }, - { url = "https://files.pythonhosted.org/packages/0c/67/5d766f9cd304e871a5dfe5f0a85eaa533538ef07e9b2858fdf9f37f83694/pydantic_monty-0.0.17-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f0688a1fa5dc045ac7b7e996d7269b94f74f116d7b1e352c7b5bb5ad53d4fe03", size = 7450119, upload-time = "2026-04-22T20:13:44.515Z" }, - { url = "https://files.pythonhosted.org/packages/33/98/fa16779021d93edb19807e87cdba56bbec6adfad21f10b41a212572ce513/pydantic_monty-0.0.17-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7b35121ac555ed201405c69d531e4cb916da6984b8cd2e15c8a319117349faf6", size = 7967398, upload-time = "2026-04-22T20:13:55.576Z" }, - { url = "https://files.pythonhosted.org/packages/92/64/287a42720bc9e975ab5b52625aa9fc6bcef8298dd821022cc45c6ee1808d/pydantic_monty-0.0.17-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c97dc44af25d4392b474902fc40f78f09fe8f44ba791364670334fe12abc077", size = 8198835, upload-time = "2026-04-22T20:15:02.072Z" }, - { url = "https://files.pythonhosted.org/packages/97/37/03edb1fd582b79b2b462afc3fea5e1c8fea73afefc4870dc35fc3c7c492e/pydantic_monty-0.0.17-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ed2fb365ef9ca921de9a17786ecfa2efe06e65678e6ca57be51658a2a880f31", size = 7739241, upload-time = "2026-04-22T20:13:46.925Z" }, - { url = "https://files.pythonhosted.org/packages/11/0b/b765c9ca2ae27def1caa07345aba073ae1239fc2d9cca7a375f3dc2195f7/pydantic_monty-0.0.17-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:5bf9f07b38dd12747e3c95b169a5afb3e2e9107622e01e548246c84d19a69c99", size = 7316719, upload-time = "2026-04-22T20:14:13.058Z" }, - { url = "https://files.pythonhosted.org/packages/e4/3b/64fe872cd575ab5262e1ba2959554ead198c939cfaa425f7f8e9b1ad2694/pydantic_monty-0.0.17-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:a5e9bafd4b5acbc0a8e12ee8403a3ce37281c3b0fa5909d3f412bee76c69003c", size = 7769150, upload-time = "2026-04-22T20:13:57.784Z" }, - { url = "https://files.pythonhosted.org/packages/76/b4/b6a0bb41f39bac2e11e6a2fd42ca0886893fafa6344fb17c3f0a94e22e83/pydantic_monty-0.0.17-cp313-cp313-win32.whl", hash = "sha256:1c239ae3e610d3f39cd1609285209a4e2d046b465ac1bfed0d4374c615eed0fa", size = 7227705, upload-time = "2026-04-22T20:15:12.262Z" }, - { url = "https://files.pythonhosted.org/packages/86/e7/d8cd62f537f7ab17714ee19ea221a0e341dab407215376ab1c41d79794c9/pydantic_monty-0.0.17-cp313-cp313-win_amd64.whl", hash = "sha256:1886c3590b02f359ae991f1e76691064f167330eda4fbf22762127ce17d0eb48", size = 8043469, upload-time = "2026-04-22T20:14:24.86Z" }, - { url = "https://files.pythonhosted.org/packages/69/c0/8354baf835e1a04c4b9e11d253f82df7d625a9305e6a23a177fb895b1484/pydantic_monty-0.0.17-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:a166bd04d1996f0d144fbb5e1391cd1c0fbdacd4fa3b689dd48931388679fe98", size = 7341303, upload-time = "2026-04-22T20:14:35.653Z" }, - { url = "https://files.pythonhosted.org/packages/00/ac/d58221b5e17915421ca00bb08b805ac121b6accb194785d5422da4a2f5fc/pydantic_monty-0.0.17-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d82f319e3fd79707a7b81bbb68596509d14ac73502d2f0daf4ab5d281efdfbdc", size = 7318912, upload-time = "2026-04-22T20:13:59.966Z" }, - { url = "https://files.pythonhosted.org/packages/81/3f/8eeb8f652f6cd6e06a737aa9f00de2949e37a669b31756bc51b6182457b4/pydantic_monty-0.0.17-cp314-cp314-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:f38b9875f7ff56fe69538b60b2ecbdcbe2b8b7780407ceb05fe0ee1414bf8d19", size = 7867027, upload-time = "2026-04-22T20:13:51.426Z" }, - { url = "https://files.pythonhosted.org/packages/55/ba/ec6620c27c8b4cada6ce53378c52c245e238e50a20b968cafdc1b8573c4e/pydantic_monty-0.0.17-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:74a778bb5a4dcdbc85b3b9002f9a72a43fb6ffd88635bfdd502e8d3053008337", size = 7137542, upload-time = "2026-04-22T20:13:35.939Z" }, - { url = "https://files.pythonhosted.org/packages/41/78/5419785630511b54b15cfb094871bcb53ec9025ba8d91bd7ab5b22b6c98f/pydantic_monty-0.0.17-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9e2ab074a65738e9c1b4be9a432e7ca1e9987a6706018dc7a5af6d4ce7cecdf3", size = 7450222, upload-time = "2026-04-22T20:13:53.378Z" }, - { url = "https://files.pythonhosted.org/packages/61/04/cec11fa96a47034da3c21af53e73f43d2270f5ce96cd710809859fe9c0b2/pydantic_monty-0.0.17-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00fd1cd28b4200c9ccd02629868486b366af1bfe1f0584d4c9e513b7a941a868", size = 7967405, upload-time = "2026-04-22T20:14:03.826Z" }, - { url = "https://files.pythonhosted.org/packages/81/e3/f2be0fb975100b6936ca36a8410098f10fab3b26729c0b0d1de2fac59ff3/pydantic_monty-0.0.17-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:26ea6684555bfd00cbe9d2df3e73caada3d82200c168c63cc475197b55b88401", size = 8199028, upload-time = "2026-04-22T20:13:26.802Z" }, - { url = "https://files.pythonhosted.org/packages/2c/43/358bdaa9c50d21fe4a25a71d43ce9af2d6796616fa47aca84f807433564e/pydantic_monty-0.0.17-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f804a03a3bbd0cf0ade1d4ce11b50ca6858e9c4440b27c746faf1d3c0a272954", size = 7749903, upload-time = "2026-04-22T20:15:09.626Z" }, - { url = "https://files.pythonhosted.org/packages/9a/da/8bcd0a78abf13edceca36aaf5c180fad963e4c2e042fd7247b9e96048306/pydantic_monty-0.0.17-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:b5476e6c08b86b0bea554b97ce9b142aac1177447f2d3c5751864b27991fd1b1", size = 7312704, upload-time = "2026-04-22T20:14:33.668Z" }, - { url = "https://files.pythonhosted.org/packages/88/49/5de8bb7f8b82c3ebb8f2485e0b7a40055b072193039d95dcb5d35fcba72c/pydantic_monty-0.0.17-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:b5fcdcca45439844bee268686f37226dbf5803ec7a5945f5536c41419f151dac", size = 7768902, upload-time = "2026-04-22T20:14:29.389Z" }, - { url = "https://files.pythonhosted.org/packages/f0/7c/500a002f1a52f17c8b8a989875da3e1590c18ce95c89856aa967e2348a36/pydantic_monty-0.0.17-cp314-cp314-win32.whl", hash = "sha256:4dd3e6e80a415e7272f7a7583a4f8e045096653f6074e181117eb61fc8fe3b45", size = 7227049, upload-time = "2026-04-22T20:14:01.871Z" }, - { url = "https://files.pythonhosted.org/packages/5c/20/b8f552f2a863778f49ebb708f18aaf0bfb275480a48965786e06efacf1c4/pydantic_monty-0.0.17-cp314-cp314-win_amd64.whl", hash = "sha256:36a8090a628e8cf91df8f66c721a71050ac8f48473d4992b9afbd9585941a647", size = 8062999, upload-time = "2026-04-22T20:14:44.006Z" }, + { url = "https://files.pythonhosted.org/packages/ff/74/36d50926a7b53b85723960fad50b34b5fc8da79cc8f6091a1f1b44a02b79/pydantic_monty-0.0.18-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:857b62bfc6f06cd9853d4fc51011391e0431187fe9d08034ae24eafcb797c60a", size = 8464519, upload-time = "2026-05-29T08:30:49.301Z" }, + { url = "https://files.pythonhosted.org/packages/28/7b/941e3c9c4816864a2c260df63d3be36c523022732154d2853e5376fcf1e1/pydantic_monty-0.0.18-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:65918fac0835109de6f725069d0aa35b7454c26809634d5344d7b26686754381", size = 8719689, upload-time = "2026-05-29T08:30:27.115Z" }, + { url = "https://files.pythonhosted.org/packages/31/20/84cfdf92732651e68aa52d846a22ae573294241b4aa75ae84c0b3d2782b0/pydantic_monty-0.0.18-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c5bee11eecbadf03b2e764feb11fdea12a6b176bf071bb2fa922a23a704a83b4", size = 9042115, upload-time = "2026-05-29T08:29:18.039Z" }, + { url = "https://files.pythonhosted.org/packages/23/dc/e3dcdef2d0dc09751ed054c69c2363e05d94c985994197ce5748b22b8799/pydantic_monty-0.0.18-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:de65d5a8c7ba74794d7f50dfa0b36014931abe2a5abe1a48778afc1bb7dd5d60", size = 8171553, upload-time = "2026-05-29T08:30:51.772Z" }, + { url = "https://files.pythonhosted.org/packages/61/93/45d2b8867f74ddff0a45b96e78d1ff5bdd4bfcd68f6fd622009096b4324c/pydantic_monty-0.0.18-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8694f0897d611d6f81901eee31d5c73c6d677cd50efe95368b40e1dc1d034e8a", size = 8586169, upload-time = "2026-05-29T08:29:58.806Z" }, + { url = "https://files.pythonhosted.org/packages/06/2c/e46629bf65a4017e905db9b87158253869d329cb884604be78e74c0e3d88/pydantic_monty-0.0.18-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dcd3286f6b74a959acd32cdb4c3f0a423f91ff6d7775a08315091766f74a76dc", size = 9181554, upload-time = "2026-05-29T08:31:03.712Z" }, + { url = "https://files.pythonhosted.org/packages/20/f6/91af3acf83fe6b156134e90e7739ff167247d7a48aa53735b3b6a050a335/pydantic_monty-0.0.18-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aa2cc2dda0c7a271c6b0792ce7e60dd0bc5114263b83dccb147c6d0c88d28614", size = 9286056, upload-time = "2026-05-29T08:30:17.643Z" }, + { url = "https://files.pythonhosted.org/packages/f0/71/1b008c633a4767e518e4aebfd79eb1c2c20259282853b6967373d70ca0f9/pydantic_monty-0.0.18-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e87e5953fe1ad15f9e67c5dc590ae240889da28bc84c344e255579d4f33281f5", size = 9266143, upload-time = "2026-05-29T08:30:01.364Z" }, + { url = "https://files.pythonhosted.org/packages/6b/c5/d2b44995729c884f682e499fea134f7b19883b3414c077431d80dc222802/pydantic_monty-0.0.18-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:83b82b7c235943081b31eb9a4c8a4af961640cfbc5b7d3a97dda1bf3efd83cff", size = 8350637, upload-time = "2026-05-29T08:30:20.061Z" }, + { url = "https://files.pythonhosted.org/packages/6e/7d/8326aca20b563cf656a2d7e52fca1ec98c9b2cde67eba06ecebffb5b73f7/pydantic_monty-0.0.18-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0526e5222cbb4cd0a253f49bfcf851dc87984b39d2a5e4eb041d8ce7d1b6987a", size = 8900794, upload-time = "2026-05-29T08:31:24.604Z" }, + { url = "https://files.pythonhosted.org/packages/8f/a6/f62f187a1327ae3bf101de44439b62508da7895ca63c38295115c12a1006/pydantic_monty-0.0.18-cp310-cp310-win32.whl", hash = "sha256:668a4502e9bd67c7bb5d2c4c9d153e9f798d4f5f452845b9a72aaa1f8ce86ab8", size = 8280979, upload-time = "2026-05-29T08:30:47.128Z" }, + { url = "https://files.pythonhosted.org/packages/8c/62/455b679f3b5c00caf362b2388d8a191889f2496f834500989be404175997/pydantic_monty-0.0.18-cp310-cp310-win_amd64.whl", hash = "sha256:12c2ac68f2a12ac68bcd51beb1bf6c2e5fd81061584fd5a826d3454fc9220e36", size = 9482422, upload-time = "2026-05-29T08:31:10.421Z" }, + { url = "https://files.pythonhosted.org/packages/8f/50/06720fb35b73993aa9964403eff1ab35b1d7bd0db1b1ee0633e19311e254/pydantic_monty-0.0.18-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:5140382a6ea68778c76f04ccb91fdbfd1a77b8cae3a89534e23a0e5afaf21e75", size = 8464367, upload-time = "2026-05-29T08:29:46.855Z" }, + { url = "https://files.pythonhosted.org/packages/6c/8e/b3946ee663349fb35f9dceddf1aed394b8e5df1d8767b840844db9cee515/pydantic_monty-0.0.18-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:421d1b7956e06a22dc13fe6a34bebf3a1bdde8cf78616eded018f7a9ca746295", size = 8718281, upload-time = "2026-05-29T08:31:06.121Z" }, + { url = "https://files.pythonhosted.org/packages/36/3f/9fb2e8d0ed660d0e5b281316be0c1cb1a023b156c02a8dc8a2c3ec007af7/pydantic_monty-0.0.18-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:6609de4408ad54387ecd0b3eedce796497ee72c6ec888074519afcd4f6959a81", size = 9041289, upload-time = "2026-05-29T08:29:33.062Z" }, + { url = "https://files.pythonhosted.org/packages/d3/5e/cb242ba7bd63985eee94f0dff8864002bb5ded2da7190e73786ddcd5b4e8/pydantic_monty-0.0.18-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd2b37890d8a948606be184bd6e3fa4e445d26e3c7329c6a451a271bfc470f24", size = 8170676, upload-time = "2026-05-29T08:29:38.358Z" }, + { url = "https://files.pythonhosted.org/packages/f1/8d/d144775ea57b813e97aef9edaf5f867fb82960597af517125e1b513c983c/pydantic_monty-0.0.18-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:19b86e4fc4b73c2c925906bbc31488b9e8b99b54101f8ea7bbdccfd60ded38f0", size = 8585337, upload-time = "2026-05-29T08:31:27.206Z" }, + { url = "https://files.pythonhosted.org/packages/b3/81/6291a4871fbdb8dfa66d1b1f2406c11066757caa1c53092320e5d11ea49d/pydantic_monty-0.0.18-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4de83f38b3658152697c524ea87ce307a13701d807801c9be27870e837829a02", size = 9181594, upload-time = "2026-05-29T08:31:36.736Z" }, + { url = "https://files.pythonhosted.org/packages/d7/00/28879cee77e24f70c756c4603b4a21b013e601b7821bd07e06cf6718b75a/pydantic_monty-0.0.18-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ef3aaa4fb7af8fd84f42df5e1e43d7ff3eae7d81314580462c8ca716e7e6e361", size = 9285193, upload-time = "2026-05-29T08:30:58.727Z" }, + { url = "https://files.pythonhosted.org/packages/9f/07/52dece571ef47085d2f1053df4c1be8d5b42d8735da4797f5f79d650f81f/pydantic_monty-0.0.18-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d3dd72c195eca243b08c5d68b1f513124feaf22acb87b73cd8bfcdc3f6b4bb7", size = 9264997, upload-time = "2026-05-29T08:29:49.51Z" }, + { url = "https://files.pythonhosted.org/packages/38/12/b010315be2927c5be43d3a4036cd6857d9981d5116efda5e40540f43a014/pydantic_monty-0.0.18-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:6b0409a5f314af54f704d73cd97fe2a0cc8ef2635952bf57c5879b9313281614", size = 8350432, upload-time = "2026-05-29T08:30:10.984Z" }, + { url = "https://files.pythonhosted.org/packages/84/8b/9674a90269dc0f1a080e606cba642b256e138e7fcee1a3a0b55969946f83/pydantic_monty-0.0.18-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:790f169bb5700e3ab24a8d44fd7016d915c701e27bcd2feabe0de917306bbff0", size = 8900736, upload-time = "2026-05-29T08:29:42.508Z" }, + { url = "https://files.pythonhosted.org/packages/28/6a/07351c22208814c466d9a26bab03642727e9f5570562cbd4fbde837e4644/pydantic_monty-0.0.18-cp311-cp311-win32.whl", hash = "sha256:3682f3bd67ef92ecd78a3f5f4efcd7659ba643aaca45412601a92691d6440250", size = 8280476, upload-time = "2026-05-29T08:31:17.814Z" }, + { url = "https://files.pythonhosted.org/packages/eb/de/937dcc0e828d324f037a5004f97c8ff245158fe735107023a10d8f672e32/pydantic_monty-0.0.18-cp311-cp311-win_amd64.whl", hash = "sha256:eecdf1175542ac2fd3f6a203c7744145be4e73e08755c6de1d35253dc6a872e7", size = 9480905, upload-time = "2026-05-29T08:30:15.287Z" }, + { url = "https://files.pythonhosted.org/packages/f2/d1/307df5ac3a694acc5922f00fc7ce96357ad4afaa41bbfeec0b8379bed6ec/pydantic_monty-0.0.18-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:1030bd49b813e67aedf4f7bb3dd4cc9edaa203554b3b8fe11eeab6d61139229f", size = 8462571, upload-time = "2026-05-29T08:29:21.081Z" }, + { url = "https://files.pythonhosted.org/packages/55/83/8ccf04b2f9642153702c6eb22d0a0abad57014fd85879ab1f6341b5a1946/pydantic_monty-0.0.18-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2988d3e511131680d9de60647bfe5c697b1e4e4cad474fecf1451c53314e8520", size = 8688756, upload-time = "2026-05-29T08:30:24.677Z" }, + { url = "https://files.pythonhosted.org/packages/81/84/e3ce3294636b92a5eb238273026dd2825d97deac44f76e901990a4eeb306/pydantic_monty-0.0.18-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:7d8d0f42162cb40da05f32d50d9d8d74411b3d4f1182117c8365f18457442c0d", size = 9046635, upload-time = "2026-05-29T08:30:06.38Z" }, + { url = "https://files.pythonhosted.org/packages/de/b8/c7881620a812850772ae0924863d1399cbecb3e4c8c455a9c7a9c20b06f8/pydantic_monty-0.0.18-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5c688dc7c7b28a2f389a61217bae1d50658e28f960abe33a254328cadc8a17a", size = 8171342, upload-time = "2026-05-29T08:29:44.773Z" }, + { url = "https://files.pythonhosted.org/packages/d3/ea/6d10ea1657e303295a75a3854f6dd6b378cbd501dcd1782844107b932acd/pydantic_monty-0.0.18-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f469293f5a776231b9617787a5dd6c58048c6568833ee6848338be6391f15449", size = 8591152, upload-time = "2026-05-29T08:31:29.944Z" }, + { url = "https://files.pythonhosted.org/packages/93/fb/ab85c4676ccffd0f3b7f509a4c8b396b07c7860577def2f58a22b3fe8aef/pydantic_monty-0.0.18-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e6e0cd4991947b8a47210985836f94c14139bc3ea06253d2f38d0d752b165517", size = 9183064, upload-time = "2026-05-29T08:31:12.776Z" }, + { url = "https://files.pythonhosted.org/packages/5c/12/11292178b487052f9e0a1ea7b3d17e1e3bfcba598fefce8cb9ed8712021e/pydantic_monty-0.0.18-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:58b4b96863abbc0ffa5baf64779b3fbb6376cc763488b027ecaddb5052d5ff17", size = 9285440, upload-time = "2026-05-29T08:29:35.642Z" }, + { url = "https://files.pythonhosted.org/packages/79/42/7afb8dde4414d84c042f2cc1b0870a7351cae2e4fbf3fef89b3aa683eca9/pydantic_monty-0.0.18-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c1208bd5976c1254b2705559836511c86ea3cc51d9c6f688b1e3e22984715dbc", size = 9233438, upload-time = "2026-05-29T08:31:39.177Z" }, + { url = "https://files.pythonhosted.org/packages/5f/46/89124cf146725e354b44685b477da6b0b5dc07a8a3af2aec309e88c55405/pydantic_monty-0.0.18-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:977168253d8f6b49bb64f128d02c4b62ee8b435fd62876268377e1f2b00cc00f", size = 8351900, upload-time = "2026-05-29T08:31:08.348Z" }, + { url = "https://files.pythonhosted.org/packages/00/c5/dda512f5a9c68242faea368844aacefb54c2a13f9b40bee5ab48ccdc78c5/pydantic_monty-0.0.18-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:c21319a091dc1ff1fccb8647dae5bb543b3f528c556319ed15c7992dfa9b5e87", size = 8901559, upload-time = "2026-05-29T08:29:26.047Z" }, + { url = "https://files.pythonhosted.org/packages/45/97/496655362d4bb6e74ff791cf40be6a502e794c296f0089912783325075a7/pydantic_monty-0.0.18-cp312-cp312-win32.whl", hash = "sha256:220fe77920af9033ae644887e747b68567df630b1a8afa39b0a830d84a3438b5", size = 8277428, upload-time = "2026-05-29T08:30:35.322Z" }, + { url = "https://files.pythonhosted.org/packages/cc/24/2913a50a9afbce681629408814ae94929589bed9aa347b176caf17842957/pydantic_monty-0.0.18-cp312-cp312-win_amd64.whl", hash = "sha256:f965a62993bd3fe7be94f99c86349d61d987b3d8cc07fb729d7d8af87c7d481d", size = 9453897, upload-time = "2026-05-29T08:31:15.481Z" }, + { url = "https://files.pythonhosted.org/packages/70/86/5f1eb8b0743ba65821aa37285f131478672b2832baa08386e931c9e71969/pydantic_monty-0.0.18-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:765865634c2075ec816515db22acf0c71e42d25dcbf66638dc063d95c7d1a858", size = 8473615, upload-time = "2026-05-29T08:30:22.027Z" }, + { url = "https://files.pythonhosted.org/packages/c6/9c/7628423f955efb669d2cc1d3a8909bf8271b543ce27036e18229ad0e51e8/pydantic_monty-0.0.18-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d47976a18e3e3da0e86f8cf6068fc8a125930422dc10d3d3bf0b5410a9e9282e", size = 8689116, upload-time = "2026-05-29T08:29:30.906Z" }, + { url = "https://files.pythonhosted.org/packages/4c/dd/ec6cbbe997205063c679ef17220a48fcb0a4c319fc336ca81a3c7c248c6d/pydantic_monty-0.0.18-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:1c6bc7a776d9d97b899054263c0f0c7316523571da02b4c2a6d2ecd4793482e0", size = 9045884, upload-time = "2026-05-29T08:30:53.831Z" }, + { url = "https://files.pythonhosted.org/packages/c2/9c/51f8ffa4340bc1986eb9240b0756724f5fdf3c463d6d66c8cc8450e1446d/pydantic_monty-0.0.18-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eb84fe51e3f6e00a0cc9628e0acf5904d982c8cc85d4db42b7532d071602f703", size = 8178458, upload-time = "2026-05-29T08:30:09.015Z" }, + { url = "https://files.pythonhosted.org/packages/c9/ec/7eb84aeb86631571f9acffc91552217dc2b524b00db37b8d10517df467d1/pydantic_monty-0.0.18-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a2e86f3ba67b094d498bc8b071d5e3b8b034bb9f3006216a357c5f71d49d6132", size = 8591295, upload-time = "2026-05-29T08:29:23.357Z" }, + { url = "https://files.pythonhosted.org/packages/da/69/d5210208fa116593bd81789e2e5abb6222d38087c9c1879e18f7e7620275/pydantic_monty-0.0.18-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eb8a412aa0336d4e0334a4e05a54b391d91fa334020bd295a280285ba17ab5ca", size = 9184647, upload-time = "2026-05-29T08:29:51.852Z" }, + { url = "https://files.pythonhosted.org/packages/ed/74/4d95c8f65072964c4cb798dbe87d2e1c1349607ab5905874bfa8a0b94de1/pydantic_monty-0.0.18-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1056ce3acef60ab880314caf97775c5ea41b30f9306d0dd28cefeffd42dda366", size = 9291637, upload-time = "2026-05-29T08:31:19.966Z" }, + { url = "https://files.pythonhosted.org/packages/d0/40/5817780313a3e089ca6f860fbdc836d3aa33790eb72c2e8fe2edc877820e/pydantic_monty-0.0.18-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9593caa45b68fd07ac67bea9974effbe2a1c5453d8a106b913596b0dff6d8471", size = 9233863, upload-time = "2026-05-29T08:31:42.838Z" }, + { url = "https://files.pythonhosted.org/packages/b3/55/f77565c5797502c7ba995dc23a26759cb33023590f9f2926bc4e8ab87afe/pydantic_monty-0.0.18-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:e15e18ed27a17ee607ad3bcbf82f25a9ec4d496ff493ff64cdabb83ca2174cec", size = 8358264, upload-time = "2026-05-29T08:31:32.531Z" }, + { url = "https://files.pythonhosted.org/packages/1b/1f/c700eb800868d1be4078a99cb00e23fb7e5d8760c8e83b729bba27b5bf92/pydantic_monty-0.0.18-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:96d75a418d96640ff0c7f354a78fc4470a2d0f40ba69e886c2f32756d594d9a0", size = 8906664, upload-time = "2026-05-29T08:30:04.055Z" }, + { url = "https://files.pythonhosted.org/packages/7f/52/1b4599d5a6dccc65d46956431cfdf5a46df4a18005a93ffec4aab56a47b8/pydantic_monty-0.0.18-cp313-cp313-win32.whl", hash = "sha256:5cd5ff08e6749b3a4a2192856861b36feee8575e2cf81bd6bd1d8b4b39ac630e", size = 8276949, upload-time = "2026-05-29T08:30:12.968Z" }, + { url = "https://files.pythonhosted.org/packages/8e/eb/54c9011e2ef5e1358512ea23bb4a862b4f8fdda2d2f951a58854ff55ee3e/pydantic_monty-0.0.18-cp313-cp313-win_amd64.whl", hash = "sha256:52ce98be1e5bf76974597234ec857b7a6ef99374a036860cd2e2e1bd75c18f1e", size = 9453909, upload-time = "2026-05-29T08:31:01.275Z" }, + { url = "https://files.pythonhosted.org/packages/9a/04/e6462c2d4097189fc4af62b84273d6ef0a69473cbdf1c7f158d8cec25c11/pydantic_monty-0.0.18-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:8c38add825895ecfde75f3272126f07dd94f2db08440f165e76d7e321feec8da", size = 8473729, upload-time = "2026-05-29T08:30:32.648Z" }, + { url = "https://files.pythonhosted.org/packages/65/ff/6aca0ddd5c074b2757dd992b38e57f5e6b21ec0631007ec2d1b4dcbd3bff/pydantic_monty-0.0.18-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:186eaa80945c4a5bb19beba54471d423bffcf5daf64f93029b2d5df1939e201f", size = 8702897, upload-time = "2026-05-29T08:29:56.669Z" }, + { url = "https://files.pythonhosted.org/packages/29/37/d56705a23d7c5ff5112f5f82d56e70a9073a46d6ebfdbce09bcb31a52921/pydantic_monty-0.0.18-cp314-cp314-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c7a568fd6db2389743d0d28f355273c0d6d2009c5252c0971dcb1e5629e60d4f", size = 9046049, upload-time = "2026-05-29T08:30:56.314Z" }, + { url = "https://files.pythonhosted.org/packages/c6/00/82a6ddb1ca7bf2b1ef4b1751d960671324f3a0a498fc80d335f3f0962176/pydantic_monty-0.0.18-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:324443ea73eb70bfd57b34e554278f52501592c4580edc6b9708b0d3d6a42f44", size = 8178495, upload-time = "2026-05-29T08:31:34.62Z" }, + { url = "https://files.pythonhosted.org/packages/71/9b/1a1aab97a113d718d6e6db9a7e5ad2fb9fd9cde8623d4885188983fa349b/pydantic_monty-0.0.18-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:936438027363474eecc5573eb635d1c5f3bf061ad02334d5b545a132fbb76e45", size = 8593356, upload-time = "2026-05-29T08:30:37.618Z" }, + { url = "https://files.pythonhosted.org/packages/65/89/0f5212ccae4c29fa85c86dea553e45cf4729f94eba47187c747d44f7c890/pydantic_monty-0.0.18-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f700d2c7139f44ac29f51301c835a23e6c53402984cb23f3e3214e47df7ddaa3", size = 9184371, upload-time = "2026-05-29T08:29:28.574Z" }, + { url = "https://files.pythonhosted.org/packages/26/1a/a2f3f0016a1326ef50d732f53bd8f447b3535fdb59a40287e77d0914935f/pydantic_monty-0.0.18-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:10abc11ae00d712b866b2a64e6a0f34c6a7d5f99903228f4699eeb4ced50299a", size = 9292432, upload-time = "2026-05-29T08:30:30.051Z" }, + { url = "https://files.pythonhosted.org/packages/78/55/8bc4f8924c8bfd366b2c524a19083f86bb457c61f56c47e4ae4bd607536e/pydantic_monty-0.0.18-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8183aa8e2420aa4c1924cc05b87c8143f953b7c173f240cb7cf20e0b3f865cdb", size = 9247001, upload-time = "2026-05-29T08:30:40.061Z" }, + { url = "https://files.pythonhosted.org/packages/5c/5e/f6ae7d18cfc058f4df49765420800dd5d7de3adbba6be864a8bc919847d2/pydantic_monty-0.0.18-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:03fccf00fd925b616e0b7ce59c354f3fa1e50eb2d511391e260e28793b5d3b0c", size = 8357641, upload-time = "2026-05-29T08:29:40.372Z" }, + { url = "https://files.pythonhosted.org/packages/ac/d3/166961ca42ad855b7a2dd50d494be26ff0222b21b68750081962fb4568f9/pydantic_monty-0.0.18-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:d9dc4185bad6ca7f38d2d71b9d8ed2d68e48e4c4f0ccc89cd0188c08469bda7d", size = 8905773, upload-time = "2026-05-29T08:30:43.535Z" }, + { url = "https://files.pythonhosted.org/packages/df/88/d8bd8e82ca624ca6bebe9ebfb6cfc461214303158ba735751a6c2277043e/pydantic_monty-0.0.18-cp314-cp314-win32.whl", hash = "sha256:4840805ecfe5a38c07126f02181d907c687ca765a73aaabc2b128184390a2c52", size = 8277373, upload-time = "2026-05-29T08:31:22.247Z" }, + { url = "https://files.pythonhosted.org/packages/b6/0e/c395b22ddc32c746d7e2d271dd18bb585289576bb67483435e25643b7ecd/pydantic_monty-0.0.18-cp314-cp314-win_amd64.whl", hash = "sha256:83b6e2b73b0fa60c5641ecb6e8b588840023163ca6ab8b3e5da7ad088390ee7c", size = 9467375, upload-time = "2026-05-29T08:29:54.227Z" }, ] [[package]] From 10b158baf33f5e7f8b4f616f115d54739fe0fa88 Mon Sep 17 00:00:00 2001 From: Martin Styk <mart.styk@gmail.com> Date: Sun, 2 Aug 2026 15:42:06 +0200 Subject: [PATCH 28/53] Forward enable_cimd to Auth0, AWS Cognito and OCI providers (#4719) Signed-off-by: Martin Styk <mart.styk@gmail.com> --- fastmcp_slim/fastmcp/server/auth/providers/auth0.py | 4 ++++ fastmcp_slim/fastmcp/server/auth/providers/aws.py | 4 ++++ fastmcp_slim/fastmcp/server/auth/providers/oci.py | 4 ++++ 3 files changed, 12 insertions(+) diff --git a/fastmcp_slim/fastmcp/server/auth/providers/auth0.py b/fastmcp_slim/fastmcp/server/auth/providers/auth0.py index e6a939b51..0a3120a2b 100644 --- a/fastmcp_slim/fastmcp/server/auth/providers/auth0.py +++ b/fastmcp_slim/fastmcp/server/auth/providers/auth0.py @@ -104,6 +104,7 @@ class Auth0Provider(OIDCProxy): fallback_refresh_token_expiry_seconds: int | None = None, fastmcp_access_token_expiry_seconds: int | None = None, token_expiry_threshold_seconds: int = 0, + enable_cimd: bool = True, ) -> None: """Initialize Auth0 OAuth provider. @@ -148,6 +149,8 @@ class Auth0Provider(OIDCProxy): refresh gracefully (e.g. `mcp-remote`). See `OAuthProxy` for details. token_expiry_threshold_seconds: Number of seconds before actual expiry to treat a token as expired, refreshing early to avoid races. Defaults to 0. + enable_cimd: Enable CIMD (Client ID Metadata Document) support for URL-based + client IDs (default True). Set to False to disable. """ # Parse scopes if provided as string auth0_required_scopes = ( @@ -174,6 +177,7 @@ class Auth0Provider(OIDCProxy): fallback_refresh_token_expiry_seconds=fallback_refresh_token_expiry_seconds, fastmcp_access_token_expiry_seconds=fastmcp_access_token_expiry_seconds, token_expiry_threshold_seconds=token_expiry_threshold_seconds, + enable_cimd=enable_cimd, ) logger.debug( diff --git a/fastmcp_slim/fastmcp/server/auth/providers/aws.py b/fastmcp_slim/fastmcp/server/auth/providers/aws.py index 7a3167ca6..01b1bcbd5 100644 --- a/fastmcp_slim/fastmcp/server/auth/providers/aws.py +++ b/fastmcp_slim/fastmcp/server/auth/providers/aws.py @@ -144,6 +144,7 @@ class AWSCognitoProvider(OIDCProxy): fallback_refresh_token_expiry_seconds: int | None = None, fastmcp_access_token_expiry_seconds: int | None = None, token_expiry_threshold_seconds: int = 0, + enable_cimd: bool = True, ): """Initialize AWS Cognito OAuth provider. @@ -188,6 +189,8 @@ class AWSCognitoProvider(OIDCProxy): refresh gracefully (e.g. `mcp-remote`). See `OAuthProxy` for details. token_expiry_threshold_seconds: Number of seconds before actual expiry to treat a token as expired, refreshing early to avoid races. Defaults to 0. + enable_cimd: Enable CIMD (Client ID Metadata Document) support for URL-based + client IDs (default True). Set to False to disable. """ # Parse scopes if provided as string required_scopes_final = ( @@ -223,6 +226,7 @@ class AWSCognitoProvider(OIDCProxy): fallback_refresh_token_expiry_seconds=fallback_refresh_token_expiry_seconds, fastmcp_access_token_expiry_seconds=fastmcp_access_token_expiry_seconds, token_expiry_threshold_seconds=token_expiry_threshold_seconds, + enable_cimd=enable_cimd, ) logger.debug( diff --git a/fastmcp_slim/fastmcp/server/auth/providers/oci.py b/fastmcp_slim/fastmcp/server/auth/providers/oci.py index ffd98ed09..613efd69b 100644 --- a/fastmcp_slim/fastmcp/server/auth/providers/oci.py +++ b/fastmcp_slim/fastmcp/server/auth/providers/oci.py @@ -141,6 +141,7 @@ class OCIProvider(OIDCProxy): fallback_refresh_token_expiry_seconds: int | None = None, fastmcp_access_token_expiry_seconds: int | None = None, token_expiry_threshold_seconds: int = 0, + enable_cimd: bool = True, ) -> None: """Initialize OCI OIDC provider. @@ -174,6 +175,8 @@ class OCIProvider(OIDCProxy): refresh gracefully (e.g. `mcp-remote`). See `OAuthProxy` for details. token_expiry_threshold_seconds: Number of seconds before actual expiry to treat a token as expired, refreshing early to avoid races. Defaults to 0. + enable_cimd: Enable CIMD (Client ID Metadata Document) support for URL-based + client IDs (default True). Set to False to disable. """ # Parse scopes if provided as string oci_required_scopes = ( @@ -200,6 +203,7 @@ class OCIProvider(OIDCProxy): fallback_refresh_token_expiry_seconds=fallback_refresh_token_expiry_seconds, fastmcp_access_token_expiry_seconds=fastmcp_access_token_expiry_seconds, token_expiry_threshold_seconds=token_expiry_threshold_seconds, + enable_cimd=enable_cimd, ) logger.debug( From c428a08feae9f0dd84db09c20cf1e745069d43d8 Mon Sep 17 00:00:00 2001 From: Shuying <46500487+ShuyingZhang@users.noreply.github.com> Date: Sun, 2 Aug 2026 08:42:26 -0500 Subject: [PATCH 29/53] fix: preserve valid servers during CLI discovery (#4714) Co-authored-by: Shuying <zsy@u.northwestern.edu> --- fastmcp_slim/fastmcp/cli/discovery.py | 39 ++++++++++++++++----------- tests/cli/test_discovery.py | 24 +++++++++++++++++ 2 files changed, 48 insertions(+), 15 deletions(-) diff --git a/fastmcp_slim/fastmcp/cli/discovery.py b/fastmcp_slim/fastmcp/cli/discovery.py index ff3c616ff..fcc6a49d5 100644 --- a/fastmcp_slim/fastmcp/cli/discovery.py +++ b/fastmcp_slim/fastmcp/cli/discovery.py @@ -97,24 +97,33 @@ def _parse_mcp_servers( if not servers_dict: return [] - normalized = { - name: _normalize_server_entry(entry) - for name, entry in servers_dict.items() - if isinstance(entry, dict) - } + discovered: list[DiscoveredServer] = [] + for name, entry in servers_dict.items(): + if not isinstance(entry, dict): + continue - try: - config = MCPConfig.from_dict({"mcpServers": normalized}) - except Exception as exc: - logger.warning("Could not parse MCP servers from %s: %s", config_path, exc) - return [] + normalized = _normalize_server_entry(entry) + try: + config = MCPConfig.from_dict({"mcpServers": {name: normalized}}) + except Exception as exc: + logger.warning( + "Could not parse MCP server %r from %s: %s", + name, + config_path, + exc, + ) + continue - return [ - DiscoveredServer( - name=name, source=source, config=server, config_path=config_path + discovered.append( + DiscoveredServer( + name=name, + source=source, + config=config.mcpServers[name], + config_path=config_path, + ) ) - for name, server in config.mcpServers.items() - ] + + return discovered def _parse_mcp_config(path: Path, source: str) -> list[DiscoveredServer]: diff --git a/tests/cli/test_discovery.py b/tests/cli/test_discovery.py index 102bbf440..6318cc9f7 100644 --- a/tests/cli/test_discovery.py +++ b/tests/cli/test_discovery.py @@ -140,6 +140,30 @@ class TestParseMcpConfig: servers = _parse_mcp_config(path, "test") assert servers == [] + def test_invalid_server_does_not_hide_valid_servers( + self, tmp_path: Path, caplog: pytest.LogCaptureFixture + ): + path = tmp_path / "config.json" + _write_config( + path, + { + "mcpServers": { + "working": { + "command": "python", + "args": ["server.py"], + }, + "broken": { + "args": ["missing-command.py"], + }, + } + }, + ) + + servers = _parse_mcp_config(path, "test") + + assert [server.name for server in servers] == ["working"] + assert "broken" in caplog.text + def test_remote_server(self, tmp_path: Path): path = tmp_path / "config.json" _write_config(path, _REMOTE_CONFIG) From 34bdd480c91fef4482b184fcc637e574edcfaecd Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Sun, 2 Aug 2026 14:11:26 -0400 Subject: [PATCH 30/53] Improve HTTP server startup performance (#4729) --- fastmcp_slim/fastmcp/server/__init__.py | 28 ++- fastmcp_slim/fastmcp/server/context.py | 19 +- fastmcp_slim/fastmcp/server/event_store.py | 55 +----- fastmcp_slim/fastmcp/server/http.py | 2 +- .../fastmcp/server/mixins/transport.py | 2 +- .../local_provider/decorators/tools.py | 18 +- fastmcp_slim/fastmcp/server/server.py | 30 +++- .../server/session_scoped_event_store.py | 68 +++++++ fastmcp_slim/fastmcp/tools/base.py | 47 +++-- .../fastmcp/tools/function_parsing.py | 14 +- .../fastmcp/utilities/docstring_parsing.py | 6 +- fastmcp_slim/fastmcp/utilities/json_schema.py | 11 +- fastmcp_slim/fastmcp/utilities/prefab.py | 74 ++++++++ scripts/benchmark_http_startup.py | 169 ++++++++++++++++++ tests/server/http/test_startup_imports.py | 73 ++++++++ 15 files changed, 488 insertions(+), 128 deletions(-) create mode 100644 fastmcp_slim/fastmcp/server/session_scoped_event_store.py create mode 100644 fastmcp_slim/fastmcp/utilities/prefab.py create mode 100644 scripts/benchmark_http_startup.py create mode 100644 tests/server/http/test_startup_imports.py diff --git a/fastmcp_slim/fastmcp/server/__init__.py b/fastmcp_slim/fastmcp/server/__init__.py index d6edbc4f1..63c1f0351 100644 --- a/fastmcp_slim/fastmcp/server/__init__.py +++ b/fastmcp_slim/fastmcp/server/__init__.py @@ -1,17 +1,31 @@ import importlib +from typing import TYPE_CHECKING from fastmcp import _install_hints -try: - from .context import Context - from .server import FastMCP, create_proxy -except ImportError as exc: - raise ImportError(_install_hints.SERVER_SUPPORT) from exc +if TYPE_CHECKING: + from .context import Context as Context + from .server import FastMCP as FastMCP + from .server import create_proxy as create_proxy def __getattr__(name: str) -> object: - if name == "dependencies": - return importlib.import_module("fastmcp.server.dependencies") + if name in {"context", "dependencies"}: + return importlib.import_module(f"fastmcp.server.{name}") + if name == "Context": + try: + from .context import Context + except ImportError as exc: + raise ImportError(_install_hints.SERVER_SUPPORT) from exc + + return Context + if name in {"FastMCP", "create_proxy"}: + try: + from .server import FastMCP, create_proxy + except ImportError as exc: + raise ImportError(_install_hints.SERVER_SUPPORT) from exc + + return FastMCP if name == "FastMCP" else create_proxy raise AttributeError(f"module {__name__!r} has no attribute {name!r}") diff --git a/fastmcp_slim/fastmcp/server/context.py b/fastmcp_slim/fastmcp/server/context.py index bd089289a..3373e09da 100644 --- a/fastmcp_slim/fastmcp/server/context.py +++ b/fastmcp_slim/fastmcp/server/context.py @@ -10,7 +10,6 @@ from logging import Logger from typing import Any, Literal, cast, overload import mcp_types -from key_value.aio.errors import SerializationError from mcp import LoggingLevel, ServerSession from mcp.server.context import ServerRequestContext from mcp_types import ( @@ -1146,10 +1145,9 @@ class Context: value=StateValue(value=value), ttl=self._STATE_TTL_SECONDS, ) - except (ValueError, SerializationError) as e: + except ValueError as e: # Pydantic raises PydanticSerializationError (a ValueError) and the - # key_value library raises SerializationError; both carry "serialize" - # in the message. Other ValueErrors propagate unchanged. + # message carries "serialize". Other ValueErrors propagate unchanged. if "serialize" in str(e).lower(): raise TypeError( f"Value for state key {key!r} is not serializable. " @@ -1158,6 +1156,19 @@ class Context: f"request-scoped and will not persist across requests." ) from e raise + except Exception as e: + # Import the optional storage implementation only on its error path, + # rather than adding the key_value package to every server startup. + from key_value.aio.errors import SerializationError + + if not isinstance(e, SerializationError): + raise + raise TypeError( + f"Value for state key {key!r} is not serializable. " + f"Use set_state({key!r}, value, serializable=False) to store " + f"non-serializable values. Note: non-serializable state is " + f"request-scoped and will not persist across requests." + ) from e async def get_state(self, key: str) -> Any: """Get a value from the state store. diff --git a/fastmcp_slim/fastmcp/server/event_store.py b/fastmcp_slim/fastmcp/server/event_store.py index 86897aac7..bdc504865 100644 --- a/fastmcp_slim/fastmcp/server/event_store.py +++ b/fastmcp_slim/fastmcp/server/event_store.py @@ -18,6 +18,9 @@ from mcp.server.streamable_http import EventStore as SDKEventStore from mcp_types import JSONRPCMessage from pydantic import TypeAdapter +from fastmcp.server.session_scoped_event_store import ( + SessionScopedEventStore as SessionScopedEventStore, +) from fastmcp.utilities.logging import get_logger from fastmcp.utilities.types import FastMCPBaseModel @@ -42,58 +45,6 @@ class StreamEventList(FastMCPBaseModel): event_ids: list[str] -class SessionScopedEventStore(SDKEventStore): - """EventStore adapter that isolates stream IDs to one transport session.""" - - def __init__(self, event_store: SDKEventStore, session_id: str): - self._event_store = event_store - self._stream_prefix = f"{len(session_id)}:{session_id}:" - - def _scope_stream_id(self, stream_id: StreamId) -> StreamId: - return f"{self._stream_prefix}{stream_id}" - - def _unscope_stream_id(self, stream_id: StreamId) -> StreamId | None: - if not stream_id.startswith(self._stream_prefix): - return None - return stream_id[len(self._stream_prefix) :] - - async def store_event( - self, stream_id: StreamId, message: JSONRPCMessage | None - ) -> EventId: - return await self._event_store.store_event( - self._scope_stream_id(stream_id), message - ) - - async def replay_events_after( - self, - last_event_id: EventId, - send_callback: EventCallback, - ) -> StreamId | None: - replayed_events: list[EventMessage] = [] - - async def buffer_event(event: EventMessage) -> None: - replayed_events.append(event) - - scoped_stream_id = await self._event_store.replay_events_after( - last_event_id, buffer_event - ) - if scoped_stream_id is None: - return None - - stream_id = self._unscope_stream_id(scoped_stream_id) - if stream_id is None: - logger.warning( - "Event ID %s does not belong to this session-scoped event store", - last_event_id, - ) - return None - - for event in replayed_events: - await send_callback(event) - - return stream_id - - class EventStore(SDKEventStore): """EventStore implementation backed by AsyncKeyValue. diff --git a/fastmcp_slim/fastmcp/server/http.py b/fastmcp_slim/fastmcp/server/http.py index f196627b7..7bd8fff99 100644 --- a/fastmcp_slim/fastmcp/server/http.py +++ b/fastmcp_slim/fastmcp/server/http.py @@ -27,7 +27,7 @@ from starlette.types import ASGIApp, Lifespan, Receive, Scope, Send from fastmcp.server.auth import AuthProvider from fastmcp.server.auth.middleware import RequireAuthMiddleware -from fastmcp.server.event_store import SessionScopedEventStore +from fastmcp.server.session_scoped_event_store import SessionScopedEventStore from fastmcp.utilities.logging import get_logger if TYPE_CHECKING: diff --git a/fastmcp_slim/fastmcp/server/mixins/transport.py b/fastmcp_slim/fastmcp/server/mixins/transport.py index ddc96db20..13bef1ced 100644 --- a/fastmcp_slim/fastmcp/server/mixins/transport.py +++ b/fastmcp_slim/fastmcp/server/mixins/transport.py @@ -11,13 +11,13 @@ import anyio import uvicorn from mcp.server.lowlevel.server import NotificationOptions from mcp.server.stdio import stdio_server +from mcp.server.streamable_http import EventStore from starlette.middleware import Middleware as ASGIMiddleware from starlette.requests import Request from starlette.responses import Response from starlette.routing import BaseRoute, Route import fastmcp -from fastmcp.server.event_store import EventStore from fastmcp.server.http import ( HostOriginProtection, StarletteWithLifespan, diff --git a/fastmcp_slim/fastmcp/server/providers/local_provider/decorators/tools.py b/fastmcp_slim/fastmcp/server/providers/local_provider/decorators/tools.py index 77081165a..3f4b322a5 100644 --- a/fastmcp_slim/fastmcp/server/providers/local_provider/decorators/tools.py +++ b/fastmcp_slim/fastmcp/server/providers/local_provider/decorators/tools.py @@ -28,17 +28,10 @@ from mcp_types import ToolAnnotations from fastmcp.tools.base import Tool from fastmcp.tools.function_tool import FunctionTool from fastmcp.utilities.authorization import AuthCheck +from fastmcp.utilities.prefab import is_prefab_type, prefab_available from fastmcp.utilities.tasks import TaskConfig from fastmcp.utilities.types import AnyFunction, NotSet, NotSetT -try: - from prefab_ui.app import PrefabApp as _PrefabApp - from prefab_ui.components.base import Component as _PrefabComponent - - _HAS_PREFAB = True -except ImportError: - _HAS_PREFAB = False - if TYPE_CHECKING: from fastmcp.server.providers.local_provider import LocalProvider @@ -51,7 +44,7 @@ PREFAB_RENDERER_URI = "ui://prefab/renderer.html" def _is_prefab_type(tp: Any) -> bool: """Check if *tp* is or contains a prefab type, recursing through unions and Annotated.""" - if isinstance(tp, type) and issubclass(tp, (_PrefabApp, _PrefabComponent)): + if is_prefab_type(tp): return True origin = get_origin(tp) if origin is Union or origin is types.UnionType or origin is Annotated: @@ -61,7 +54,7 @@ def _is_prefab_type(tp: Any) -> bool: def _has_prefab_return_type(tool: Tool) -> bool: """Check if a FunctionTool's return type annotation is a prefab type.""" - if not _HAS_PREFAB or not isinstance(tool, FunctionTool): + if not isinstance(tool, FunctionTool): return False rt = tool.return_type if rt is None or rt is inspect.Parameter.empty: @@ -94,13 +87,10 @@ def _maybe_apply_prefab_ui(provider: LocalProvider, tool: Tool) -> None: it. ``app=True``, return-type inference, and ``PrefabAppConfig`` all funnel through the same placeholder marker. """ - if not _HAS_PREFAB: - return - meta = tool.meta or {} ui = meta.get("ui") - if ui is True: + if ui is True and prefab_available(): # Explicit app=True: stamp the placeholder so the synthesizer finds it. _stamp_prefab_marker(tool) elif ui is None and _has_prefab_return_type(tool): diff --git a/fastmcp_slim/fastmcp/server/server.py b/fastmcp_slim/fastmcp/server/server.py index 6fb97bace..a25a78712 100644 --- a/fastmcp_slim/fastmcp/server/server.py +++ b/fastmcp_slim/fastmcp/server/server.py @@ -20,9 +20,6 @@ from typing import TYPE_CHECKING, Any, Generic, Literal, TypeVar, cast, overload import httpx2 import mcp_types -from key_value.aio.adapters.pydantic import PydanticAdapter -from key_value.aio.protocols import AsyncKeyValue -from key_value.aio.stores.memory import MemoryStore from mcp.server.lowlevel.server import LifespanResultT from mcp.server.request_state import RequestStateSecurity from mcp.shared.exceptions import MCPError @@ -101,6 +98,9 @@ from fastmcp.utilities.versions import ( ) if TYPE_CHECKING: + from key_value.aio.adapters.pydantic import PydanticAdapter + from key_value.aio.protocols import AsyncKeyValue + from fastmcp.client import Client from fastmcp.client.client import SDKServer from fastmcp.client.transports import ClientTransport, ClientTransportT @@ -333,12 +333,8 @@ class FastMCP( self._additional_http_routes: list[BaseRoute] = [] # Session-scoped state store (shared across all requests) - self._state_storage: AsyncKeyValue = session_state_store or MemoryStore() - self._state_store: PydanticAdapter[StateValue] = PydanticAdapter[StateValue]( - key_value=self._state_storage, - pydantic_model=StateValue, - default_collection="fastmcp_state", - ) + self._state_storage: AsyncKeyValue | None = session_state_store + self.__state_store: PydanticAdapter[StateValue] | None = None # Create LocalProvider for local components self._local_provider: LocalProvider = LocalProvider( @@ -496,6 +492,22 @@ class FastMCP( def __repr__(self) -> str: return f"{type(self).__name__}({self.name!r})" + @property + def _state_store(self) -> PydanticAdapter[StateValue]: + """Create the session-state adapter only when state is first used.""" + if self.__state_store is None: + from key_value.aio.adapters.pydantic import PydanticAdapter + from key_value.aio.stores.memory import MemoryStore + + if self._state_storage is None: + self._state_storage = MemoryStore() + self.__state_store = PydanticAdapter[StateValue]( + key_value=self._state_storage, + pydantic_model=StateValue, + default_collection="fastmcp_state", + ) + return self.__state_store + @property def name(self) -> str: return self._mcp_server.name diff --git a/fastmcp_slim/fastmcp/server/session_scoped_event_store.py b/fastmcp_slim/fastmcp/server/session_scoped_event_store.py new file mode 100644 index 000000000..9d0adada4 --- /dev/null +++ b/fastmcp_slim/fastmcp/server/session_scoped_event_store.py @@ -0,0 +1,68 @@ +"""Lightweight session scoping for Streamable HTTP event stores.""" + +from __future__ import annotations + +from mcp.server.streamable_http import ( + EventCallback, + EventId, + EventMessage, + EventStore, + StreamId, +) +from mcp_types import JSONRPCMessage + +from fastmcp.utilities.logging import get_logger + +logger = get_logger(__name__) + + +class SessionScopedEventStore(EventStore): + """EventStore adapter that isolates stream IDs to one transport session.""" + + def __init__(self, event_store: EventStore, session_id: str): + self._event_store = event_store + self._stream_prefix = f"{len(session_id)}:{session_id}:" + + def _scope_stream_id(self, stream_id: StreamId) -> StreamId: + return f"{self._stream_prefix}{stream_id}" + + def _unscope_stream_id(self, stream_id: StreamId) -> StreamId | None: + if not stream_id.startswith(self._stream_prefix): + return None + return stream_id[len(self._stream_prefix) :] + + async def store_event( + self, stream_id: StreamId, message: JSONRPCMessage | None + ) -> EventId: + return await self._event_store.store_event( + self._scope_stream_id(stream_id), message + ) + + async def replay_events_after( + self, + last_event_id: EventId, + send_callback: EventCallback, + ) -> StreamId | None: + replayed_events: list[EventMessage] = [] + + async def buffer_event(event: EventMessage) -> None: + replayed_events.append(event) + + scoped_stream_id = await self._event_store.replay_events_after( + last_event_id, buffer_event + ) + if scoped_stream_id is None: + return None + + stream_id = self._unscope_stream_id(scoped_stream_id) + if stream_id is None: + logger.warning( + "Event ID %s does not belong to this session-scoped event store", + last_event_id, + ) + return None + + for event in replayed_events: + await send_callback(event) + + return stream_id diff --git a/fastmcp_slim/fastmcp/tools/base.py b/fastmcp_slim/fastmcp/tools/base.py index 5e02246dd..ccba1f365 100644 --- a/fastmcp_slim/fastmcp/tools/base.py +++ b/fastmcp_slim/fastmcp/tools/base.py @@ -26,6 +26,11 @@ from pydantic.json_schema import SkipJsonSchema from fastmcp.utilities.authorization import AuthCheck from fastmcp.utilities.components import FastMCPComponent from fastmcp.utilities.logging import get_logger +from fastmcp.utilities.prefab import ( + is_prefab_app, + is_prefab_component, + prefab_app_from_component, +) from fastmcp.utilities.tasks import TaskConfig from fastmcp.utilities.types import ( Audio, @@ -35,14 +40,6 @@ from fastmcp.utilities.types import ( NotSetT, ) -try: - from prefab_ui.app import PrefabApp as _PrefabApp - from prefab_ui.components.base import Component as _PrefabComponent - - _HAS_PREFAB = True -except ImportError: - _HAS_PREFAB = False - if TYPE_CHECKING: from fastmcp.tools.function_tool import FunctionTool from fastmcp.tools.tool_transform import ArgTransform, TransformedTool @@ -128,13 +125,12 @@ class ToolResult(BaseModel): if structured_content is not None: # Convert Prefab types to their wire-format envelope before # generic serialization, so the renderer gets the right shape. - if _HAS_PREFAB: - if isinstance(structured_content, _PrefabApp): - structured_content = _prefab_to_json(structured_content) - elif isinstance(structured_content, _PrefabComponent): - structured_content = _prefab_to_json( - _PrefabApp(view=structured_content) - ) + if is_prefab_app(structured_content): + structured_content = _prefab_to_json(structured_content) + elif is_prefab_component(structured_content): + structured_content = _prefab_to_json( + prefab_app_from_component(structured_content) + ) try: structured_content = pydantic_core.to_jsonable_python( @@ -379,17 +375,16 @@ class Tool(FastMCPComponent): if isinstance(raw_value, CallToolResult): return ToolResult.from_mcp_result(raw_value) - if _HAS_PREFAB: - if isinstance(raw_value, _PrefabApp): - return _prefab_to_tool_result( - raw_value, - fastmcp_app_name=_get_fastmcp_app_name(self), - ) - if isinstance(raw_value, _PrefabComponent): - return _prefab_to_tool_result( - _PrefabApp(view=raw_value), - fastmcp_app_name=_get_fastmcp_app_name(self), - ) + if is_prefab_app(raw_value): + return _prefab_to_tool_result( + raw_value, + fastmcp_app_name=_get_fastmcp_app_name(self), + ) + if is_prefab_component(raw_value): + return _prefab_to_tool_result( + prefab_app_from_component(raw_value), + fastmcp_app_name=_get_fastmcp_app_name(self), + ) content = _convert_to_content(raw_value) diff --git a/fastmcp_slim/fastmcp/tools/function_parsing.py b/fastmcp_slim/fastmcp/tools/function_parsing.py index cc42550fc..5972e7d0d 100644 --- a/fastmcp_slim/fastmcp/tools/function_parsing.py +++ b/fastmcp_slim/fastmcp/tools/function_parsing.py @@ -18,6 +18,7 @@ from fastmcp.tools.base import ToolResult, resolve_serialize_by_alias from fastmcp.utilities.docstring_parsing import ParsedDocstring, parse_docstring from fastmcp.utilities.json_schema import compress_schema from fastmcp.utilities.logging import get_logger +from fastmcp.utilities.prefab import is_prefab_type from fastmcp.utilities.types import ( Audio, File, @@ -27,14 +28,6 @@ from fastmcp.utilities.types import ( replace_type, ) -try: - from prefab_ui.app import PrefabApp as _PrefabApp - from prefab_ui.components.base import Component as _PrefabComponent - - _PREFAB_TYPES: tuple[type, ...] = (_PrefabApp, _PrefabComponent) -except ImportError: - _PREFAB_TYPES = () - def _contains_bytes_type(tp: Any) -> bool: """Check if *tp* is or contains bytes, recursing through unions and Annotated.""" @@ -48,7 +41,7 @@ def _contains_bytes_type(tp: Any) -> bool: def _contains_prefab_type(tp: Any) -> bool: """Check if *tp* is or contains a prefab type, recursing through unions and Annotated.""" - if isinstance(tp, type) and issubclass(tp, _PREFAB_TYPES): + if is_prefab_type(tp): return True origin = get_origin(tp) if origin is Union or origin is types.UnionType or origin is Annotated: @@ -405,7 +398,7 @@ class ParsedFunction: # so we handle subclass matching explicitly here. We also need # to handle composite types like ``Column | None`` and # ``Annotated[PrefabApp, ...]`` by recursing into their args. - if _PREFAB_TYPES and _contains_prefab_type(output_type): + if _contains_prefab_type(output_type): output_type = _UnserializableType # ToolResult subclasses should suppress schema generation just @@ -450,7 +443,6 @@ class ParsedFunction: # A guard tool's suspend signal is control flow, not # output data (any residual bare arm is suppressed). mcp_types.InputRequiredResult, - *_PREFAB_TYPES, ), _UnserializableType, ), diff --git a/fastmcp_slim/fastmcp/utilities/docstring_parsing.py b/fastmcp_slim/fastmcp/utilities/docstring_parsing.py index babcb1e96..111f37657 100644 --- a/fastmcp_slim/fastmcp/utilities/docstring_parsing.py +++ b/fastmcp_slim/fastmcp/utilities/docstring_parsing.py @@ -14,8 +14,6 @@ from collections.abc import Callable from dataclasses import dataclass, field from typing import Any -from griffe import Docstring, DocstringSectionKind - _PARSERS = ("google", "numpy", "sphinx") logger = logging.getLogger("griffe") @@ -43,6 +41,10 @@ def parse_docstring(fn: Callable[..., Any]) -> ParsedDocstring: if not doc: return ParsedDocstring() + # Griffe is only needed for functions that actually have docstrings. This + # keeps its parser and model graph out of ordinary server startup. + from griffe import Docstring, DocstringSectionKind + # Try each parser and use the first one that finds parameters. for parser in _PARSERS: docstring = Docstring(doc, lineno=1, parser=parser) diff --git a/fastmcp_slim/fastmcp/utilities/json_schema.py b/fastmcp_slim/fastmcp/utilities/json_schema.py index 59715e8ed..533a4a7bf 100644 --- a/fastmcp_slim/fastmcp/utilities/json_schema.py +++ b/fastmcp_slim/fastmcp/utilities/json_schema.py @@ -3,7 +3,12 @@ from __future__ import annotations from collections import defaultdict from typing import Any -from jsonref import JsonRefError, replace_refs + +def replace_refs(*args: Any, **kwargs: Any) -> Any: + """Call jsonref lazily while preserving the module's patchable boundary.""" + from jsonref import replace_refs as _replace_refs + + return _replace_refs(*args, **kwargs) def _copy_schema(schema: dict[str, Any]) -> dict[str, Any]: @@ -221,6 +226,10 @@ def dereference_refs(schema: dict[str, Any]) -> dict[str, Any]: if _defs_have_cycles(schema.get("$defs", {})): return resolve_root_ref(schema) + # Most schema operations do not dereference. Keep jsonref (and its requests + # dependency tree) out of server startup until a schema actually needs it. + from jsonref import JsonRefError + try: # Use jsonref to resolve all $ref references # proxies=False returns plain dicts (not proxy objects) diff --git a/fastmcp_slim/fastmcp/utilities/prefab.py b/fastmcp_slim/fastmcp/utilities/prefab.py new file mode 100644 index 000000000..805a5fede --- /dev/null +++ b/fastmcp_slim/fastmcp/utilities/prefab.py @@ -0,0 +1,74 @@ +"""Lazy helpers for FastMCP's optional Prefab UI integration.""" + +from __future__ import annotations + +import sys +from functools import lru_cache +from importlib.util import find_spec +from typing import Any + + +@lru_cache(maxsize=1) +def prefab_available() -> bool: + """Return whether Prefab UI is installed without importing it.""" + return find_spec("prefab_ui") is not None + + +@lru_cache(maxsize=1) +def _get_prefab_types() -> tuple[type[Any], type[Any]] | None: + """Import and return Prefab's public app and component types on demand.""" + if not prefab_available(): + return None + + from prefab_ui.app import PrefabApp + from prefab_ui.components.base import Component + + return PrefabApp, Component + + +def _could_be_prefab(value_or_type: Any) -> bool: + """Cheaply reject ordinary values before importing Prefab UI.""" + candidate_type = ( + value_or_type if isinstance(value_or_type, type) else type(value_or_type) + ) + module = getattr(candidate_type, "__module__", "") + return ( + "prefab_ui" in sys.modules + or module == "prefab_ui" + or module.startswith("prefab_ui.") + ) + + +def is_prefab_type(candidate: Any) -> bool: + """Return whether a type is a Prefab app or component type.""" + if not isinstance(candidate, type) or not _could_be_prefab(candidate): + return False + + prefab_types = _get_prefab_types() + return prefab_types is not None and issubclass(candidate, prefab_types) + + +def is_prefab_app(value: Any) -> bool: + """Return whether a value is a Prefab app.""" + if not _could_be_prefab(value): + return False + + prefab_types = _get_prefab_types() + return prefab_types is not None and isinstance(value, prefab_types[0]) + + +def is_prefab_component(value: Any) -> bool: + """Return whether a value is a Prefab component.""" + if not _could_be_prefab(value): + return False + + prefab_types = _get_prefab_types() + return prefab_types is not None and isinstance(value, prefab_types[1]) + + +def prefab_app_from_component(component: Any) -> Any: + """Wrap a Prefab component in a Prefab app.""" + prefab_types = _get_prefab_types() + if prefab_types is None or not isinstance(component, prefab_types[1]): + raise TypeError("Expected a Prefab UI component") + return prefab_types[0](view=component) diff --git a/scripts/benchmark_http_startup.py b/scripts/benchmark_http_startup.py new file mode 100644 index 000000000..54162ec4f --- /dev/null +++ b/scripts/benchmark_http_startup.py @@ -0,0 +1,169 @@ +#!/usr/bin/env python +"""Benchmark FastMCP's HTTP server cold-start path in fresh interpreters. + +The benchmark separates the work users pay before an HTTP server can accept +requests: + +1. import the public ``FastMCP`` entry point; +2. construct a server and register representative tools; +3. build the Streamable HTTP ASGI application. + +Every sample runs in a fresh interpreter. Use ratios and the shape of the +results rather than treating single-machine absolute timings as universal. + +Usage: + uv run python scripts/benchmark_http_startup.py + uv run python scripts/benchmark_http_startup.py --runs 10 + uv run python scripts/benchmark_http_startup.py --json +""" + +from __future__ import annotations + +import argparse +import json +import statistics +import subprocess +import sys +import textwrap +from collections.abc import Sequence +from typing import TypedDict + + +class Sample(TypedDict): + import_ms: float + server_ms: float + app_ms: float + total_ms: float + module_count: int + rss_mib: float + heavy_module_counts: dict[str, int] + + +_PROBE = textwrap.dedent( + """ + import json + import resource + import sys + import time + + started = time.perf_counter() + from fastmcp import FastMCP + imported = time.perf_counter() + + server = FastMCP("HTTP cold-start benchmark") + + def make_tool(index): + def tool(value: int = index) -> int: + return value + + tool.__name__ = f"tool_{index}" + return tool + + for index in range(10): + server.tool(make_tool(index)) + configured = time.perf_counter() + + app = server.http_app(transport="http", stateless_http=True) + assert app is not None + ready = time.perf_counter() + + heavy_roots = { + "authlib", + "cryptography", + "httpx2", + "key_value", + "mcp", + "mcp_types", + "opentelemetry", + "pydantic", + "rich", + "sse_starlette", + "starlette", + "uvicorn", + } + heavy_module_counts = { + root: sum( + module == root or module.startswith(f"{root}.") for module in sys.modules + ) + for root in sorted(heavy_roots) + } + heavy_module_counts = { + root: count for root, count in heavy_module_counts.items() if count + } + + print( + json.dumps( + { + "import_ms": (imported - started) * 1000, + "server_ms": (configured - imported) * 1000, + "app_ms": (ready - configured) * 1000, + "total_ms": (ready - started) * 1000, + "module_count": len(sys.modules), + "rss_mib": ( + resource.getrusage(resource.RUSAGE_SELF).ru_maxrss + / (1024 * 1024) + if sys.platform == "darwin" + else resource.getrusage(resource.RUSAGE_SELF).ru_maxrss / 1024 + ), + "heavy_module_counts": heavy_module_counts, + } + ) + ) + """ +) + + +def _sample() -> Sample: + result = subprocess.run( + [sys.executable, "-c", _PROBE], + capture_output=True, + text=True, + check=False, + timeout=30, + ) + if result.returncode != 0: + raise RuntimeError(result.stderr) + return json.loads(result.stdout.strip().splitlines()[-1]) + + +def _median(samples: Sequence[Sample], key: str) -> float: + return statistics.median(float(sample[key]) for sample in samples) # type: ignore[literal-required] + + +def _summarize(samples: list[Sample]) -> dict[str, object]: + return { + "runs": len(samples), + "import_ms": round(_median(samples, "import_ms"), 1), + "server_ms": round(_median(samples, "server_ms"), 1), + "app_ms": round(_median(samples, "app_ms"), 1), + "total_ms": round(_median(samples, "total_ms"), 1), + "module_count": round(_median(samples, "module_count")), + "rss_mib": round(_median(samples, "rss_mib"), 1), + "heavy_module_counts": samples[-1]["heavy_module_counts"], + } + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("--runs", type=int, default=5) + parser.add_argument("--json", action="store_true") + args = parser.parse_args() + + samples = [_sample() for _ in range(args.runs)] + summary = _summarize(samples) + if args.json: + print(json.dumps(summary, indent=2)) + return + + print(f"Python: {sys.version.split()[0]}") + print(f"Runs: {summary['runs']}") + print(f"Import FastMCP: {summary['import_ms']:.1f} ms") + print(f"Construct + 10 tools: {summary['server_ms']:.1f} ms") + print(f"Build HTTP app: {summary['app_ms']:.1f} ms") + print(f"Total to ASGI app: {summary['total_ms']:.1f} ms") + print(f"Modules: {summary['module_count']}") + print(f"Peak RSS: {summary['rss_mib']:.1f} MiB") + + +if __name__ == "__main__": + main() diff --git a/tests/server/http/test_startup_imports.py b/tests/server/http/test_startup_imports.py new file mode 100644 index 000000000..52af15fc1 --- /dev/null +++ b/tests/server/http/test_startup_imports.py @@ -0,0 +1,73 @@ +"""Fresh-interpreter import guards for the default HTTP server path.""" + +from __future__ import annotations + +import subprocess +import sys +import textwrap + +import pytest + + +@pytest.mark.subprocess_heavy +def test_default_http_app_does_not_load_opt_in_integrations() -> None: + script = textwrap.dedent( + """ + import sys + + from fastmcp import FastMCP + + server = FastMCP("HTTP import guard") + + @server.tool + def echo(value: str) -> str: + return value + + app = server.http_app(transport="http", stateless_http=True) + assert app is not None + + forbidden = ( + "fastmcp.server.event_store", + "griffe", + "jsonref", + "key_value", + "prefab_ui", + ) + loaded = [ + name + for name in sys.modules + if any(name == root or name.startswith(f"{root}.") for root in forbidden) + ] + assert not loaded, loaded + """ + ) + + result = subprocess.run( + [sys.executable, "-c", script], + capture_output=True, + text=True, + ) + + assert result.returncode == 0, result.stderr + + +@pytest.mark.subprocess_heavy +def test_fastmcp_server_import_does_not_load_context() -> None: + script = textwrap.dedent( + """ + import sys + + from fastmcp import FastMCP + + assert FastMCP is not None + assert "fastmcp.server.context" not in sys.modules + """ + ) + + result = subprocess.run( + [sys.executable, "-c", script], + capture_output=True, + text=True, + ) + + assert result.returncode == 0, result.stderr From 022547ad8c957753d0b5a09290a4be345d3f01d2 Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Sun, 2 Aug 2026 14:56:05 -0400 Subject: [PATCH 31/53] Preserve string-compatible prompt arguments (#4730) --- .../fastmcp/prompts/function_prompt.py | 39 ++++++--- tests/prompts/test_prompt.py | 83 +++++++++++++++++-- 2 files changed, 104 insertions(+), 18 deletions(-) diff --git a/fastmcp_slim/fastmcp/prompts/function_prompt.py b/fastmcp_slim/fastmcp/prompts/function_prompt.py index 9685630b1..e17d17d4e 100644 --- a/fastmcp_slim/fastmcp/prompts/function_prompt.py +++ b/fastmcp_slim/fastmcp/prompts/function_prompt.py @@ -217,7 +217,10 @@ class FunctionPrompt(Prompt): schema_str = json.dumps(param_schema, separators=(",", ":")) # Append schema info to description - schema_note = f"Provide as a JSON string matching the following schema: {schema_str}" + schema_note = ( + "Provide a value matching the following JSON schema: " + f"{schema_str}. Encode non-string values as JSON." + ) if arg_description: arg_description = f"{arg_description}\n\n{schema_note}" else: @@ -263,26 +266,38 @@ class FunctionPrompt(Prompt): if param_name in sig.parameters: param = sig.parameters[param_name] - # If parameter has no annotation or annotation is str, pass as-is - if ( - param.annotation == inspect.Parameter.empty - or param.annotation is str - ) or not isinstance(param_value, str): + if param.annotation == inspect.Parameter.empty or not isinstance( + param_value, str + ): converted_kwargs[param_name] = param_value else: # Try to convert string argument using type adapter try: adapter = get_cached_typeadapter(param.annotation) - # Try JSON parsing first for complex types + # Preserve the MCP wire string when validation keeps it + # as a string. Non-string results still prefer JSON + # decoding so coercible types such as bytes and Path do + # not retain JSON quote characters. try: + python_value = adapter.validate_python(param_value) + except (ValueError, TypeError, pydantic_core.ValidationError): converted_kwargs[param_name] = adapter.validate_json( param_value ) - except (ValueError, TypeError, pydantic_core.ValidationError): - # Fallback to direct validation - converted_kwargs[param_name] = adapter.validate_python( - param_value - ) + else: + if isinstance(python_value, str): + converted_kwargs[param_name] = python_value + else: + try: + converted_kwargs[param_name] = ( + adapter.validate_json(param_value) + ) + except ( + ValueError, + TypeError, + pydantic_core.ValidationError, + ): + converted_kwargs[param_name] = python_value except (ValueError, TypeError, pydantic_core.ValidationError) as e: # If conversion fails, provide informative error raise PromptError( diff --git a/tests/prompts/test_prompt.py b/tests/prompts/test_prompt.py index 201632e01..d44668783 100644 --- a/tests/prompts/test_prompt.py +++ b/tests/prompts/test_prompt.py @@ -1,5 +1,9 @@ +from pathlib import Path +from typing import Annotated, Any + import pytest from mcp_types import EmbeddedResource, TextResourceContents +from pydantic import Field from fastmcp.prompts.base import ( Message, @@ -313,8 +317,75 @@ class TestPromptTypeConversion: assert result.messages == [Message("Hello world (repeated 3 times)")] + @pytest.mark.parametrize( + ("annotation", "value"), + [ + (Annotated[str, Field(description="Text")], '"hello"'), + (str | None, "null"), + (Any, "123"), + (object, "true"), + (int | str, "42"), + ], + ) + async def test_string_compatible_annotations_preserve_wire_strings( + self, annotation: Any, value: str + ): + def typed_prompt(value): + return f"{type(value).__name__}:{value!r}" + + typed_prompt.__annotations__ = {"value": annotation, "return": str} + prompt = Prompt.from_function(typed_prompt) + + result = await prompt.render(arguments={"value": value}) + + assert result.messages == [Message(f"str:{value!r}")] + + async def test_optional_non_string_still_decodes_json_null(self): + def optional_integer_prompt(value: int | None) -> str: + return f"{type(value).__name__}:{value!r}" + + prompt = Prompt.from_function(optional_integer_prompt) + + result = await prompt.render(arguments={"value": "null"}) + + assert result.messages == [Message("NoneType:None")] + + @pytest.mark.parametrize( + ("annotation", "value", "expected"), + [ + (bytes, '"hello"', b"hello"), + (Path, '"folder/file.txt"', Path("folder/file.txt")), + ], + ) + async def test_string_coercible_non_string_annotations_decode_json( + self, annotation: Any, value: str, expected: Any + ): + def typed_prompt(value): + return f"{type(value).__name__}:{value!r}" + + typed_prompt.__annotations__ = {"value": annotation, "return": str} + prompt = Prompt.from_function(typed_prompt) + + result = await prompt.render(arguments={"value": value}) + + assert result.messages == [Message(f"{type(expected).__name__}:{expected!r}")] + class TestPromptArgumentDescriptions: + def test_string_compatible_annotation_guidance_preserves_raw_strings(self): + def documented_prompt( + text: Annotated[str, Field(description="Text")], + ) -> str: + return text + + prompt = Prompt.from_function(documented_prompt) + + assert prompt.arguments is not None + text_arg = next(arg for arg in prompt.arguments if arg.name == "text") + assert text_arg.description is not None + assert "Provide as a JSON string" not in text_arg.description + assert "Encode non-string values as JSON." in text_arg.description + def test_enhanced_descriptions_for_non_string_types(self): """Test that non-string argument types get enhanced descriptions with JSON schema.""" @@ -343,7 +414,7 @@ class TestPromptArgumentDescriptions: assert numbers_arg is not None assert numbers_arg.description is not None assert ( - "Provide as a JSON string matching the following schema:" + "Provide a value matching the following JSON schema:" in numbers_arg.description ) assert '{"items":{"type":"integer"},"type":"array"}' in numbers_arg.description @@ -354,7 +425,7 @@ class TestPromptArgumentDescriptions: assert metadata_arg is not None assert metadata_arg.description is not None assert ( - "Provide as a JSON string matching the following schema:" + "Provide a value matching the following JSON schema:" in metadata_arg.description ) assert ( @@ -368,7 +439,7 @@ class TestPromptArgumentDescriptions: assert threshold_arg is not None assert threshold_arg.description is not None assert ( - "Provide as a JSON string matching the following schema:" + "Provide a value matching the following JSON schema:" in threshold_arg.description ) assert '{"type":"number"}' in threshold_arg.description @@ -379,7 +450,7 @@ class TestPromptArgumentDescriptions: assert active_arg is not None assert active_arg.description is not None assert ( - "Provide as a JSON string matching the following schema:" + "Provide a value matching the following JSON schema:" in active_arg.description ) assert '{"type":"boolean"}' in active_arg.description @@ -410,7 +481,7 @@ class TestPromptArgumentDescriptions: assert "A list of integers to process" in numbers_arg.description assert "\n\n" in numbers_arg.description # Should have newline separator assert ( - "Provide as a JSON string matching the following schema:" + "Provide a value matching the following JSON schema:" in numbers_arg.description ) @@ -427,7 +498,7 @@ class TestPromptArgumentDescriptions: # String parameters should not have schema enhancement if arg.description is not None: assert ( - "Provide as a JSON string matching the following schema:" + "Provide a value matching the following JSON schema:" not in arg.description ) From a7e9b709192d19a9c014d95ef4fbedc35befeeec Mon Sep 17 00:00:00 2001 From: nate nowack <thrast36@gmail.com> Date: Mon, 3 Aug 2026 10:53:23 -0500 Subject: [PATCH 32/53] Fix static analysis with latest ty (#4739) * Fix upgrade static analysis Generated with Codex * Preserve concrete transport return types Generated with Codex * Avoid widening transport return types Generated with Codex * Model transforming transport return types Generated with Codex * Exclude standalone screenshot examples from ty Generated with Codex --- fastmcp_slim/fastmcp/mcp_config.py | 9 ++++++--- pyproject.toml | 7 ++++--- tests/test_mcp_config.py | 2 +- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/fastmcp_slim/fastmcp/mcp_config.py b/fastmcp_slim/fastmcp/mcp_config.py index c09d9f3c6..b18ea1a3e 100644 --- a/fastmcp_slim/fastmcp/mcp_config.py +++ b/fastmcp_slim/fastmcp/mcp_config.py @@ -45,6 +45,7 @@ from fastmcp import _install_hints if TYPE_CHECKING: from fastmcp.client.transports import ( ClientTransport, + FastMCPTransport, SSETransport, StdioTransport, StreamableHttpTransport, @@ -153,7 +154,7 @@ class _TransformingMCPServerMixin(BaseModel): return wrapped_mcp_server, transport - def to_transport(self) -> ClientTransport: + def to_transport(self) -> FastMCPTransport: """Get the transport for the transforming MCP server.""" try: from fastmcp.client.transports import FastMCPTransport @@ -209,7 +210,7 @@ class StdioMCPServer(BaseModel): model_config = ConfigDict(extra="allow") # Preserve unknown fields - def to_transport(self) -> StdioTransport: + def to_transport(self) -> StdioTransport | FastMCPTransport: from fastmcp.client.transports import StdioTransport return StdioTransport( @@ -261,7 +262,9 @@ class RemoteMCPServer(BaseModel): extra="allow", arbitrary_types_allowed=True ) # Preserve unknown fields - def to_transport(self) -> StreamableHttpTransport | SSETransport: + def to_transport( + self, + ) -> StreamableHttpTransport | SSETransport | FastMCPTransport: from fastmcp.client.transports import ( SSETransport, StreamableHttpTransport, diff --git a/pyproject.toml b/pyproject.toml index 005194822..27b3596b0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -157,6 +157,8 @@ exclude = [ "examples/smart_home", # needs phue "examples/apps/qr_server", # needs qrcode "examples/providers/sqlite", # needs aiosqlite + "examples/fastmcp_config_demo", # needs pyautogui, Pillow + "examples/screenshot.py", # needs pyautogui, Pillow "examples/memory.py", # needs asyncpg, numpy, pydantic_ai, pgvector "examples/get_file.py", # needs aiohttp ] @@ -165,9 +167,8 @@ exclude = [ python-version = "3.10" [tool.ty.analysis] -# prefab_ui is the apps SDK; pyautogui/PIL are optional runtime deps used only -# inside example tool bodies (screenshot demos) and are not installed here. -replace-imports-with-any = ["prefab_ui.**", "pyautogui", "PIL", "PIL.**"] +# prefab_ui is the apps SDK and is not installed here. +replace-imports-with-any = ["prefab_ui.**"] [tool.ty.rules] division-by-zero = "warn" diff --git a/tests/test_mcp_config.py b/tests/test_mcp_config.py index a35af2287..93d673e61 100644 --- a/tests/test_mcp_config.py +++ b/tests/test_mcp_config.py @@ -96,7 +96,7 @@ class InMemoryStdioMCPServer(StdioMCPServer): mcp: FastMCP command: str = "in-memory" - def to_transport(self) -> FastMCPTransport: # ty: ignore[invalid-method-override] + def to_transport(self) -> FastMCPTransport: return FastMCPTransport(mcp=self.mcp) From d267792653a11faa736ffd0fff073a2f815a3b56 Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Mon, 3 Aug 2026 18:02:09 -0400 Subject: [PATCH 33/53] docs: clarify external OAuth consent mode (#4746) --- docs/servers/auth/oauth-proxy.mdx | 8 +++++--- docs/servers/auth/oidc-proxy.mdx | 2 +- docs/v2/servers/auth/oauth-proxy.mdx | 8 +++++--- docs/v2/servers/auth/oidc-proxy.mdx | 2 +- docs/v3/servers/auth/oauth-proxy.mdx | 8 +++++--- docs/v3/servers/auth/oidc-proxy.mdx | 2 +- fastmcp_slim/fastmcp/server/auth/oauth_proxy/proxy.py | 6 ++++-- fastmcp_slim/fastmcp/server/auth/oidc_proxy.py | 5 +++-- fastmcp_slim/fastmcp/server/auth/providers/auth0.py | 5 +++-- fastmcp_slim/fastmcp/server/auth/providers/aws.py | 5 +++-- fastmcp_slim/fastmcp/server/auth/providers/azure.py | 5 +++-- fastmcp_slim/fastmcp/server/auth/providers/clerk.py | 5 +++-- fastmcp_slim/fastmcp/server/auth/providers/discord.py | 5 +++-- fastmcp_slim/fastmcp/server/auth/providers/github.py | 5 +++-- fastmcp_slim/fastmcp/server/auth/providers/google.py | 5 +++-- fastmcp_slim/fastmcp/server/auth/providers/workos.py | 5 +++-- 16 files changed, 49 insertions(+), 32 deletions(-) diff --git a/docs/servers/auth/oauth-proxy.mdx b/docs/servers/auth/oauth-proxy.mdx index dd4751350..03727ec87 100644 --- a/docs/servers/auth/oauth-proxy.mdx +++ b/docs/servers/auth/oauth-proxy.mdx @@ -315,8 +315,10 @@ auth = OAuthProxy(..., client_storage=MemoryStore()) **`"remember"` — silent consent on return:** Users see the consent screen on first authorization; subsequent flows from the same browser for the same `(client_id, redirect_uri)` are silently approved via a signed cookie. Cross-site navigations (detected via `Sec-Fetch-Site`) fall back to the prompt. `Sec-Fetch-Site` is a browser-level heuristic rather than a protocol guarantee: an attacker who finds a way to initiate a non-cross-site navigation (XSS on a sibling origin, a same-site redirect chain, etc.) can reach the silent-consent path. `True` does not depend on this signal. See [Confused Deputy Attacks](#confused-deputy-attacks) for the underlying attack class. - **`"external"` — delegate to upstream:** - Skip the built-in consent page; consent is collected by the upstream IdP or a custom login page referenced via `upstream_authorization_endpoint`. No security warning is logged. + **`"external"` — externally managed:** + Follows the same authorization path as `False`: FastMCP skips its consent page and associated browser-binding protections, then redirects directly to the upstream provider. The difference is logging. `False` emits a security warning, while `"external"` suppresses that warning as an explicit acknowledgment that the operator is enforcing equivalent consent and transaction-binding protections elsewhere. FastMCP does not provide or verify those external protections. + + Ordinary upstream OAuth consent is generally not equivalent. It typically authorizes FastMCP's shared upstream application without identifying the downstream MCP client or binding approval to that client's transaction. Use `"external"` only when your surrounding authorization system supplies those protections. **`False` — disable entirely:** Authorization proceeds directly to the upstream provider without any consent UI. Logs a security warning. Only for local development or testing. @@ -336,7 +338,7 @@ auth = OAuthProxy(..., client_storage=MemoryStore()) ``` <Warning> - Disabling consent removes an important security layer. Only disable for local development or testing environments where you fully control all connecting clients. + Both `False` and `"external"` disable FastMCP's consent and browser-binding protections. `False` warns about this configuration; `"external"` suppresses the warning because it is an operator acknowledgment that equivalent protections exist elsewhere. Prefer the default `True` unless you own that external authorization flow. </Warning> </ParamField> diff --git a/docs/servers/auth/oidc-proxy.mdx b/docs/servers/auth/oidc-proxy.mdx index 006efc8d6..be4bcb22d 100644 --- a/docs/servers/auth/oidc-proxy.mdx +++ b/docs/servers/auth/oidc-proxy.mdx @@ -206,7 +206,7 @@ auth = OIDCProxy( </ParamField> <ParamField body="require_authorization_consent" type='bool | Literal["remember", "external"]' default="True"> - Consent screen behavior for authorization requests. Accepts `True` (default; always prompt — strongest protection), `"remember"` (silent consent on return visits via signed cookie, gated by `Sec-Fetch-Site` to block AS-in-the-middle attacks), `"external"` (consent handled by upstream IdP or custom page), or `False` (disable entirely; local/testing only). See the [OAuthProxy documentation](/servers/auth/oauth-proxy) for full details on each mode and the security trade-offs. + Consent screen behavior for authorization requests. Accepts `True` (default; always prompt — strongest protection), `"remember"` (silent consent on return visits via signed cookie, gated by `Sec-Fetch-Site` to block AS-in-the-middle attacks), `"external"` (same authorization path as `False`, but the warning is suppressed because the operator asserts that equivalent protections are enforced externally), or `False` (disable entirely; local/testing only). See the [OAuthProxy documentation](/servers/auth/oauth-proxy) for full details on each mode and the security trade-offs. </ParamField> <ParamField body="consent_csp_policy" type="str | None" default="None"> diff --git a/docs/v2/servers/auth/oauth-proxy.mdx b/docs/v2/servers/auth/oauth-proxy.mdx index eef3bce1c..678c396b5 100644 --- a/docs/v2/servers/auth/oauth-proxy.mdx +++ b/docs/v2/servers/auth/oauth-proxy.mdx @@ -296,8 +296,10 @@ auth = OAuthProxy(..., client_storage=MemoryStore()) **`"remember"` — silent consent on return:** Users see the consent screen on first authorization; subsequent flows from the same browser for the same `(client_id, redirect_uri)` are silently approved via a signed cookie. Cross-site navigations (detected via `Sec-Fetch-Site`) fall back to the prompt. `Sec-Fetch-Site` is a browser-level heuristic rather than a protocol guarantee: an attacker who finds a way to initiate a non-cross-site navigation (XSS on a sibling origin, a same-site redirect chain, etc.) can reach the silent-consent path. `True` does not depend on this signal. See [Confused Deputy Attacks](#confused-deputy-attacks) for the underlying attack class. - **`"external"` — delegate to upstream:** - Skip the built-in consent page; consent is collected by the upstream IdP or a custom login page referenced via `upstream_authorization_endpoint`. No security warning is logged. + **`"external"` — externally managed:** + Follows the same authorization path as `False`: FastMCP skips its consent page and associated browser-binding protections, then redirects directly to the upstream provider. The difference is logging. `False` emits a security warning, while `"external"` suppresses that warning as an explicit acknowledgment that the operator is enforcing equivalent consent and transaction-binding protections elsewhere. FastMCP does not provide or verify those external protections. + + Ordinary upstream OAuth consent is generally not equivalent. It typically authorizes FastMCP's shared upstream application without identifying the downstream MCP client or binding approval to that client's transaction. Use `"external"` only when your surrounding authorization system supplies those protections. **`False` — disable entirely:** Authorization proceeds directly to the upstream provider without any consent UI. Logs a security warning. Only for local development or testing. @@ -317,7 +319,7 @@ auth = OAuthProxy(..., client_storage=MemoryStore()) ``` <Warning> - Disabling consent removes an important security layer. Only disable for local development or testing environments where you fully control all connecting clients. + Both `False` and `"external"` disable FastMCP's consent and browser-binding protections. `False` warns about this configuration; `"external"` suppresses the warning because it is an operator acknowledgment that equivalent protections exist elsewhere. Prefer the default `True` unless you own that external authorization flow. </Warning> </ParamField> diff --git a/docs/v2/servers/auth/oidc-proxy.mdx b/docs/v2/servers/auth/oidc-proxy.mdx index 750298298..a7988995e 100644 --- a/docs/v2/servers/auth/oidc-proxy.mdx +++ b/docs/v2/servers/auth/oidc-proxy.mdx @@ -199,7 +199,7 @@ auth = OIDCProxy( </ParamField> <ParamField body="require_authorization_consent" type='bool | Literal["remember", "external"]' default="True"> - Consent screen behavior for authorization requests. Accepts `True` (default; always prompt — strongest protection), `"remember"` (silent consent on return visits via signed cookie, gated by `Sec-Fetch-Site` to block AS-in-the-middle attacks), `"external"` (consent handled by upstream IdP or custom page), or `False` (disable entirely; local/testing only). See the [OAuthProxy documentation](/v2/servers/auth/oauth-proxy) for full details on each mode and the security trade-offs. + Consent screen behavior for authorization requests. Accepts `True` (default; always prompt — strongest protection), `"remember"` (silent consent on return visits via signed cookie, gated by `Sec-Fetch-Site` to block AS-in-the-middle attacks), `"external"` (same authorization path as `False`, but the warning is suppressed because the operator asserts that equivalent protections are enforced externally), or `False` (disable entirely; local/testing only). See the [OAuthProxy documentation](/v2/servers/auth/oauth-proxy) for full details on each mode and the security trade-offs. </ParamField> <ParamField body="consent_csp_policy" type="str | None" default="None"> diff --git a/docs/v3/servers/auth/oauth-proxy.mdx b/docs/v3/servers/auth/oauth-proxy.mdx index e35f244eb..79ba7bf09 100644 --- a/docs/v3/servers/auth/oauth-proxy.mdx +++ b/docs/v3/servers/auth/oauth-proxy.mdx @@ -310,8 +310,10 @@ auth = OAuthProxy(..., client_storage=MemoryStore()) **`"remember"` — silent consent on return:** Users see the consent screen on first authorization; subsequent flows from the same browser for the same `(client_id, redirect_uri)` are silently approved via a signed cookie. Cross-site navigations (detected via `Sec-Fetch-Site`) fall back to the prompt. `Sec-Fetch-Site` is a browser-level heuristic rather than a protocol guarantee: an attacker who finds a way to initiate a non-cross-site navigation (XSS on a sibling origin, a same-site redirect chain, etc.) can reach the silent-consent path. `True` does not depend on this signal. See [Confused Deputy Attacks](#confused-deputy-attacks) for the underlying attack class. - **`"external"` — delegate to upstream:** - Skip the built-in consent page; consent is collected by the upstream IdP or a custom login page referenced via `upstream_authorization_endpoint`. No security warning is logged. + **`"external"` — externally managed:** + Follows the same authorization path as `False`: FastMCP skips its consent page and associated browser-binding protections, then redirects directly to the upstream provider. The difference is logging. `False` emits a security warning, while `"external"` suppresses that warning as an explicit acknowledgment that the operator is enforcing equivalent consent and transaction-binding protections elsewhere. FastMCP does not provide or verify those external protections. + + Ordinary upstream OAuth consent is generally not equivalent. It typically authorizes FastMCP's shared upstream application without identifying the downstream MCP client or binding approval to that client's transaction. Use `"external"` only when your surrounding authorization system supplies those protections. **`False` — disable entirely:** Authorization proceeds directly to the upstream provider without any consent UI. Logs a security warning. Only for local development or testing. @@ -331,7 +333,7 @@ auth = OAuthProxy(..., client_storage=MemoryStore()) ``` <Warning> - Disabling consent removes an important security layer. Only disable for local development or testing environments where you fully control all connecting clients. + Both `False` and `"external"` disable FastMCP's consent and browser-binding protections. `False` warns about this configuration; `"external"` suppresses the warning because it is an operator acknowledgment that equivalent protections exist elsewhere. Prefer the default `True` unless you own that external authorization flow. </Warning> </ParamField> diff --git a/docs/v3/servers/auth/oidc-proxy.mdx b/docs/v3/servers/auth/oidc-proxy.mdx index fde747e2b..81ca677a2 100644 --- a/docs/v3/servers/auth/oidc-proxy.mdx +++ b/docs/v3/servers/auth/oidc-proxy.mdx @@ -199,7 +199,7 @@ auth = OIDCProxy( </ParamField> <ParamField body="require_authorization_consent" type='bool | Literal["remember", "external"]' default="True"> - Consent screen behavior for authorization requests. Accepts `True` (default; always prompt — strongest protection), `"remember"` (silent consent on return visits via signed cookie, gated by `Sec-Fetch-Site` to block AS-in-the-middle attacks), `"external"` (consent handled by upstream IdP or custom page), or `False` (disable entirely; local/testing only). See the [OAuthProxy documentation](/servers/auth/oauth-proxy) for full details on each mode and the security trade-offs. + Consent screen behavior for authorization requests. Accepts `True` (default; always prompt — strongest protection), `"remember"` (silent consent on return visits via signed cookie, gated by `Sec-Fetch-Site` to block AS-in-the-middle attacks), `"external"` (same authorization path as `False`, but the warning is suppressed because the operator asserts that equivalent protections are enforced externally), or `False` (disable entirely; local/testing only). See the [OAuthProxy documentation](/servers/auth/oauth-proxy) for full details on each mode and the security trade-offs. </ParamField> <ParamField body="consent_csp_policy" type="str | None" default="None"> diff --git a/fastmcp_slim/fastmcp/server/auth/oauth_proxy/proxy.py b/fastmcp_slim/fastmcp/server/auth/oauth_proxy/proxy.py index 2ca7fa311..760552bcc 100644 --- a/fastmcp_slim/fastmcp/server/auth/oauth_proxy/proxy.py +++ b/fastmcp_slim/fastmcp/server/auth/oauth_proxy/proxy.py @@ -397,8 +397,10 @@ class OAuthProxy(OAuthProvider, ConsentMixin): redirect_uri) in the same browser. Cross-site navigations are still prompted to block AS-in-the-middle attacks. Lower UX friction, but weaker protection than True. - - "external": skip the built-in consent screen; consent is handled - externally (e.g. by the upstream IdP or a custom login page). + - "external": follow the same authorization path as False, but + suppress the warning as an operator acknowledgment that equivalent + consent and transaction-binding protections are enforced externally. + FastMCP does not provide or verify those external protections. - False: skip consent entirely. SECURITY WARNING: only set to False for local development or testing environments. consent_csp_policy: Content Security Policy for the consent page. diff --git a/fastmcp_slim/fastmcp/server/auth/oidc_proxy.py b/fastmcp_slim/fastmcp/server/auth/oidc_proxy.py index e2bda58ab..5e4086c7a 100644 --- a/fastmcp_slim/fastmcp/server/auth/oidc_proxy.py +++ b/fastmcp_slim/fastmcp/server/auth/oidc_proxy.py @@ -306,8 +306,9 @@ class OIDCProxy(OAuthProxy): require_authorization_consent: Whether to require user consent before authorizing clients (default True). When True, users see a consent screen before being redirected to the upstream IdP. When False, authorization proceeds directly without user confirmation. - When "external", the built-in consent screen is skipped but no warning is - logged, indicating that consent is handled externally (e.g. by the upstream IdP). + When "external", authorization follows the same direct path as False, + but the warning is suppressed as an operator acknowledgment that + equivalent protections are enforced externally. SECURITY WARNING: Only set to False for local development or testing environments. consent_csp_policy: Content Security Policy for the consent page. If None (default), uses the built-in CSP policy with appropriate directives. diff --git a/fastmcp_slim/fastmcp/server/auth/providers/auth0.py b/fastmcp_slim/fastmcp/server/auth/providers/auth0.py index 0a3120a2b..29cc02a88 100644 --- a/fastmcp_slim/fastmcp/server/auth/providers/auth0.py +++ b/fastmcp_slim/fastmcp/server/auth/providers/auth0.py @@ -135,8 +135,9 @@ class Auth0Provider(OIDCProxy): require_authorization_consent: Whether to require user consent before authorizing clients (default True). When True, users see a consent screen before being redirected to Auth0. When False, authorization proceeds directly without user confirmation. - When "external", the built-in consent screen is skipped but no warning is - logged, indicating that consent is handled externally (e.g. by the upstream IdP). + When "external", authorization follows the same direct path as False, + but the warning is suppressed as an operator acknowledgment that + equivalent protections are enforced externally. SECURITY WARNING: Only set to False for local development or testing environments. fallback_refresh_token_expiry_seconds: Lifetime for the FastMCP-issued refresh token when the upstream provider omits `refresh_expires_in` diff --git a/fastmcp_slim/fastmcp/server/auth/providers/aws.py b/fastmcp_slim/fastmcp/server/auth/providers/aws.py index 01b1bcbd5..553c9b705 100644 --- a/fastmcp_slim/fastmcp/server/auth/providers/aws.py +++ b/fastmcp_slim/fastmcp/server/auth/providers/aws.py @@ -175,8 +175,9 @@ class AWSCognitoProvider(OIDCProxy): require_authorization_consent: Whether to require user consent before authorizing clients (default True). When True, users see a consent screen before being redirected to AWS Cognito. When False, authorization proceeds directly without user confirmation. - When "external", the built-in consent screen is skipped but no warning is - logged, indicating that consent is handled externally (e.g. by the upstream IdP). + When "external", authorization follows the same direct path as False, + but the warning is suppressed as an operator acknowledgment that + equivalent protections are enforced externally. SECURITY WARNING: Only set to False for local development or testing environments. fallback_refresh_token_expiry_seconds: Lifetime for the FastMCP-issued refresh token when the upstream provider omits `refresh_expires_in` diff --git a/fastmcp_slim/fastmcp/server/auth/providers/azure.py b/fastmcp_slim/fastmcp/server/auth/providers/azure.py index b8d4332e2..b0cf9d360 100644 --- a/fastmcp_slim/fastmcp/server/auth/providers/azure.py +++ b/fastmcp_slim/fastmcp/server/auth/providers/azure.py @@ -173,8 +173,9 @@ class AzureProvider(OAuthProxy): require_authorization_consent: Whether to require user consent before authorizing clients (default True). When True, users see a consent screen before being redirected to Azure. When False, authorization proceeds directly without user confirmation. - When "external", the built-in consent screen is skipped but no warning is - logged, indicating that consent is handled externally (e.g. by the upstream IdP). + When "external", authorization follows the same direct path as False, + but the warning is suppressed as an operator acknowledgment that + equivalent protections are enforced externally. SECURITY WARNING: Only set to False for local development or testing environments. http_client: Optional httpx2.AsyncClient for connection pooling in JWKS fetches. When provided, the client is reused for JWT key fetches and the caller diff --git a/fastmcp_slim/fastmcp/server/auth/providers/clerk.py b/fastmcp_slim/fastmcp/server/auth/providers/clerk.py index 6378e5de8..cb7a20660 100644 --- a/fastmcp_slim/fastmcp/server/auth/providers/clerk.py +++ b/fastmcp_slim/fastmcp/server/auth/providers/clerk.py @@ -327,8 +327,9 @@ class ClerkProvider(OAuthProxy): into a 32-byte key. If not provided, the upstream client secret will be used to derive a 32-byte key using PBKDF2. require_authorization_consent: Whether to require user consent before authorizing - clients (default True). When "external", the built-in consent screen is skipped - but no warning is logged, indicating that consent is handled externally by Clerk. + clients (default True). When "external", authorization follows the same direct + path as False, but the warning is suppressed as an operator acknowledgment that + equivalent protections are enforced externally. consent_csp_policy: Custom CSP policy for the consent page. extra_authorize_params: Additional parameters to forward to Clerk's authorization endpoint. Example: {"prompt": "login"} to force re-authentication. diff --git a/fastmcp_slim/fastmcp/server/auth/providers/discord.py b/fastmcp_slim/fastmcp/server/auth/providers/discord.py index bdb922fa2..8a6b657b6 100644 --- a/fastmcp_slim/fastmcp/server/auth/providers/discord.py +++ b/fastmcp_slim/fastmcp/server/auth/providers/discord.py @@ -241,8 +241,9 @@ class DiscordProvider(OAuthProxy): require_authorization_consent: Whether to require user consent before authorizing clients (default True). When True, users see a consent screen before being redirected to Discord. When False, authorization proceeds directly without user confirmation. - When "external", the built-in consent screen is skipped but no warning is - logged, indicating that consent is handled externally (e.g. by the upstream IdP). + When "external", authorization follows the same direct path as False, + but the warning is suppressed as an operator acknowledgment that + equivalent protections are enforced externally. SECURITY WARNING: Only set to False for local development or testing environments. http_client: Optional httpx2.AsyncClient for connection pooling in token verification. When provided, the client is reused across verify_token calls and the caller diff --git a/fastmcp_slim/fastmcp/server/auth/providers/github.py b/fastmcp_slim/fastmcp/server/auth/providers/github.py index db6718922..214d24c3b 100644 --- a/fastmcp_slim/fastmcp/server/auth/providers/github.py +++ b/fastmcp_slim/fastmcp/server/auth/providers/github.py @@ -257,8 +257,9 @@ class GitHubProvider(OAuthProxy): require_authorization_consent: Whether to require user consent before authorizing clients (default True). When True, users see a consent screen before being redirected to GitHub. When False, authorization proceeds directly without user confirmation. - When "external", the built-in consent screen is skipped but no warning is - logged, indicating that consent is handled externally (e.g. by the upstream IdP). + When "external", authorization follows the same direct path as False, + but the warning is suppressed as an operator acknowledgment that + equivalent protections are enforced externally. SECURITY WARNING: Only set to False for local development or testing environments. http_client: Optional httpx2.AsyncClient for connection pooling in token verification. When provided, the client is reused across verify_token calls and the caller diff --git a/fastmcp_slim/fastmcp/server/auth/providers/google.py b/fastmcp_slim/fastmcp/server/auth/providers/google.py index 7a9e4ba18..a8536b223 100644 --- a/fastmcp_slim/fastmcp/server/auth/providers/google.py +++ b/fastmcp_slim/fastmcp/server/auth/providers/google.py @@ -290,8 +290,9 @@ class GoogleProvider(OAuthProxy): require_authorization_consent: Whether to require user consent before authorizing clients (default True). When True, users see a consent screen before being redirected to Google. When False, authorization proceeds directly without user confirmation. - When "external", the built-in consent screen is skipped but no warning is - logged, indicating that consent is handled externally (e.g. by Google's own consent). + When "external", authorization follows the same direct path as False, + but the warning is suppressed as an operator acknowledgment that + equivalent protections are enforced externally. SECURITY WARNING: Only set to False for local development or testing environments. extra_authorize_params: Additional parameters to forward to Google's authorization endpoint. By default, GoogleProvider sets {"access_type": "offline", "prompt": "consent"} to ensure diff --git a/fastmcp_slim/fastmcp/server/auth/providers/workos.py b/fastmcp_slim/fastmcp/server/auth/providers/workos.py index bf955e821..18c0a763a 100644 --- a/fastmcp_slim/fastmcp/server/auth/providers/workos.py +++ b/fastmcp_slim/fastmcp/server/auth/providers/workos.py @@ -213,8 +213,9 @@ class WorkOSProvider(OAuthProxy): require_authorization_consent: Whether to require user consent before authorizing clients (default True). When True, users see a consent screen before being redirected to WorkOS. When False, authorization proceeds directly without user confirmation. - When "external", the built-in consent screen is skipped but no warning is - logged, indicating that consent is handled externally (e.g. by the upstream IdP). + When "external", authorization follows the same direct path as False, + but the warning is suppressed as an operator acknowledgment that + equivalent protections are enforced externally. SECURITY WARNING: Only set to False for local development or testing environments. extra_authorize_params: Additional parameters to forward to WorkOS's authorization endpoint. Useful for forcing scopes like `offline_access` so WorkOS issues a refresh token, From 886776f5fced4b312154bf770b0662128b23936d Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Tue, 4 Aug 2026 15:25:46 -0400 Subject: [PATCH 34/53] Canonicalize response cache arguments (#4753) Co-authored-by: LHMQ878 <LHMQ878@users.noreply.github.com> --- .../fastmcp/server/middleware/caching.py | 11 ++++- tests/server/middleware/test_caching.py | 49 +++++++++++++++++++ 2 files changed, 58 insertions(+), 2 deletions(-) diff --git a/fastmcp_slim/fastmcp/server/middleware/caching.py b/fastmcp_slim/fastmcp/server/middleware/caching.py index d470f1b90..e96948fb0 100644 --- a/fastmcp_slim/fastmcp/server/middleware/caching.py +++ b/fastmcp_slim/fastmcp/server/middleware/caching.py @@ -1,6 +1,7 @@ """A middleware for response caching.""" import hashlib +import json from collections.abc import Sequence from logging import Logger from typing import Any, TypedDict @@ -593,13 +594,19 @@ class ResponseCachingMiddleware(Middleware): def _get_arguments_str(arguments: dict[str, Any] | None) -> str: - """Get a string representation of the arguments.""" + """Get a canonical string representation of the arguments.""" if arguments is None: return "null" try: - return pydantic_core.to_json(value=arguments, fallback=str).decode() + return json.dumps( + pydantic_core.to_jsonable_python(arguments, fallback=str), + ensure_ascii=False, + separators=(",", ":"), + sort_keys=True, + default=str, + ) except TypeError: return repr(arguments) diff --git a/tests/server/middleware/test_caching.py b/tests/server/middleware/test_caching.py index b45961bcf..fc7c1ffc2 100644 --- a/tests/server/middleware/test_caching.py +++ b/tests/server/middleware/test_caching.py @@ -4,6 +4,7 @@ import sys import tempfile import warnings from pathlib import Path +from typing import Any from unittest.mock import AsyncMock, MagicMock import mcp_types @@ -284,6 +285,42 @@ class TestResponseCachingMiddleware: ) assert middleware1._matches_tool_cache_settings(tool_name=tool_name) is result + @pytest.mark.parametrize( + ("first", "second"), + [ + ({"a": 5, "b": 3}, {"b": 3, "a": 5}), + ({"q": {"x": 1, "y": 2}}, {"q": {"y": 2, "x": 1}}), + ({"items": [{"x": 1, "y": 2}]}, {"items": [{"y": 2, "x": 1}]}), + ], + ids=["top level", "nested dict", "dict inside a list"], + ) + def test_call_tool_cache_key_ignores_argument_order( + self, first: dict[str, Any], second: dict[str, Any] + ): + assert _make_call_tool_cache_key( + mcp_types.CallToolRequestParams(name="tool", arguments=first) + ) == _make_call_tool_cache_key( + mcp_types.CallToolRequestParams(name="tool", arguments=second) + ) + + def test_get_prompt_cache_key_ignores_argument_order(self): + assert _make_get_prompt_cache_key( + mcp_types.GetPromptRequestParams( + name="prompt", arguments={"a": "5", "b": "3"} + ) + ) == _make_get_prompt_cache_key( + mcp_types.GetPromptRequestParams( + name="prompt", arguments={"b": "3", "a": "5"} + ) + ) + + def test_call_tool_cache_key_distinguishes_arguments(self): + assert _make_call_tool_cache_key( + mcp_types.CallToolRequestParams(name="tool", arguments={"a": 5, "b": 3}) + ) != _make_call_tool_cache_key( + mcp_types.CallToolRequestParams(name="tool", arguments={"a": 3, "b": 5}) + ) + @pytest.mark.skipif( sys.platform == "win32", @@ -424,6 +461,18 @@ class TestResponseCachingMiddlewareIntegration: ) assert call_tool_result_one == call_tool_result_two + async def test_call_tool_with_reordered_arguments_hits_cache( + self, + caching_server: FastMCP, + tracking_calculator: TrackingCalculator, + ): + async with Client[FastMCPTransport](transport=caching_server) as client: + first = await client.call_tool("add", {"a": 5, "b": 3}) + second = await client.call_tool("add", {"b": 3, "a": 5}) + + assert first == second + assert tracking_calculator.add_calls == 1 + async def test_call_tool_very_large_value( self, caching_server: FastMCP, From db92d44ef510eca7d4fbd1c3108a7482d8021ce1 Mon Sep 17 00:00:00 2001 From: LHMQ878 <72402929@cityu-dg.edu.cn> Date: Wed, 5 Aug 2026 05:11:42 +0800 Subject: [PATCH 35/53] Serve empty list results from the response cache (#4738) --- .../fastmcp/server/middleware/caching.py | 14 ++- tests/server/middleware/test_caching.py | 87 ++++++++++++++++++- 2 files changed, 97 insertions(+), 4 deletions(-) diff --git a/fastmcp_slim/fastmcp/server/middleware/caching.py b/fastmcp_slim/fastmcp/server/middleware/caching.py index e96948fb0..66918d1a5 100644 --- a/fastmcp_slim/fastmcp/server/middleware/caching.py +++ b/fastmcp_slim/fastmcp/server/middleware/caching.py @@ -355,7 +355,11 @@ class ResponseCachingMiddleware(Middleware): cache_key: str = _get_auth_partition_key() - if cached_value := await self._list_tools_cache.get(key=cache_key): + # an empty list is a cached result, not a miss: `get` returns None when the key is + # absent, so testing truthiness would re-list on every request for any caller whose + # filtered view is empty + cached_value = await self._list_tools_cache.get(key=cache_key) + if cached_value is not None: return cached_value tools: Sequence[Tool] = await call_next(context) @@ -384,7 +388,9 @@ class ResponseCachingMiddleware(Middleware): cache_key: str = _get_auth_partition_key() - if cached_value := await self._list_resources_cache.get(key=cache_key): + # an empty list is a cached result, not a miss (see on_list_tools) + cached_value = await self._list_resources_cache.get(key=cache_key) + if cached_value is not None: return cached_value resources: Sequence[Resource] = await call_next(context) @@ -415,7 +421,9 @@ class ResponseCachingMiddleware(Middleware): cache_key: str = _get_auth_partition_key() - if cached_value := await self._list_prompts_cache.get(key=cache_key): + # an empty list is a cached result, not a miss (see on_list_tools) + cached_value = await self._list_prompts_cache.get(key=cache_key) + if cached_value is not None: return cached_value prompts: Sequence[Prompt] = await call_next(context) diff --git a/tests/server/middleware/test_caching.py b/tests/server/middleware/test_caching.py index fc7c1ffc2..c2d229468 100644 --- a/tests/server/middleware/test_caching.py +++ b/tests/server/middleware/test_caching.py @@ -3,6 +3,7 @@ import sys import tempfile import warnings +from collections.abc import Sequence from pathlib import Path from typing import Any from unittest.mock import AsyncMock, MagicMock @@ -41,7 +42,11 @@ from fastmcp.server.middleware.caching import ( _make_get_prompt_cache_key, _make_read_resource_cache_key, ) -from fastmcp.server.middleware.middleware import CallNext, MiddlewareContext +from fastmcp.server.middleware.middleware import ( + CallNext, + Middleware, + MiddlewareContext, +) from fastmcp.tools.base import Tool, ToolResult from fastmcp.utilities.tasks import TaskConfig @@ -951,3 +956,83 @@ class TestAuthAwareCaching: assert {p.name for p in prompts} == {"public_prompt"} finally: auth_context_var.reset(tok) + + +class CountingDownstream(Middleware): + """Counts the list calls that get past the caching middleware, i.e. cache misses.""" + + def __init__(self) -> None: + self.list_calls = 0 + + async def on_list_tools( + self, + context: MiddlewareContext[mcp_types.ListToolsRequest], + call_next: CallNext[mcp_types.ListToolsRequest, Sequence[Tool]], + ) -> Sequence[Tool]: + self.list_calls += 1 + return await call_next(context) + + async def on_list_resources( + self, + context: MiddlewareContext[mcp_types.ListResourcesRequest], + call_next: CallNext[mcp_types.ListResourcesRequest, Sequence[Resource]], + ) -> Sequence[Resource]: + self.list_calls += 1 + return await call_next(context) + + async def on_list_prompts( + self, + context: MiddlewareContext[mcp_types.ListPromptsRequest], + call_next: CallNext[mcp_types.ListPromptsRequest, Sequence[Prompt]], + ) -> Sequence[Prompt]: + self.list_calls += 1 + return await call_next(context) + + +class TestEmptyListCaching: + """An empty list is a cached result, not a cache miss. + + Regression tests for issue #4733: the list hooks tested the cached value for + truthiness, so a server - or a per-user filtered view - with nothing to list + re-ran the listing on every single request and never served a cache hit. + """ + + @pytest.mark.parametrize("operation", ["tools", "resources", "prompts"]) + async def test_empty_list_is_served_from_cache(self, operation: str): + counter = CountingDownstream() + mcp_server = FastMCP("test", middleware=[ResponseCachingMiddleware(), counter]) + + list_operation = getattr(mcp_server, f"list_{operation}") + for _ in range(3): + assert len(await list_operation()) == 0 + + assert counter.list_calls == 1 + + async def test_empty_filtered_view_is_served_from_cache(self): + from mcp.server.auth.middleware.auth_context import auth_context_var + from mcp.server.auth.middleware.bearer_auth import AuthenticatedUser + + from fastmcp.server.auth import AccessToken, require_scopes + + counter = CountingDownstream() + mcp_server = FastMCP("test", middleware=[ResponseCachingMiddleware(), counter]) + + @mcp_server.tool(auth=require_scopes("admin")) + def admin_only() -> str: + return "ok" + + token = AccessToken( + token="token-read", + client_id="test-client", + scopes=["read"], + expires_at=None, + claims={}, + ) + tok = auth_context_var.set(AuthenticatedUser(token)) + try: + for _ in range(3): + assert len(await mcp_server.list_tools()) == 0 + finally: + auth_context_var.reset(tok) + + assert counter.list_calls == 1 From 4f28dceac87cc9e4610c01ffdc0499b4acd7c218 Mon Sep 17 00:00:00 2001 From: Sai Mouli <141447420+SaiMouli3@users.noreply.github.com> Date: Wed, 5 Aug 2026 02:45:01 +0530 Subject: [PATCH 36/53] Don't cache error results in ResponseCachingMiddleware (#4705) --- .../fastmcp/server/middleware/caching.py | 8 ++++ tests/server/middleware/test_caching.py | 45 +++++++++++++++++++ 2 files changed, 53 insertions(+) diff --git a/fastmcp_slim/fastmcp/server/middleware/caching.py b/fastmcp_slim/fastmcp/server/middleware/caching.py index 66918d1a5..a6db7eced 100644 --- a/fastmcp_slim/fastmcp/server/middleware/caching.py +++ b/fastmcp_slim/fastmcp/server/middleware/caching.py @@ -483,6 +483,14 @@ class ResponseCachingMiddleware(Middleware): if not isinstance(tool_result, ToolResult): return tool_result + # Never cache an error result. A tool that reports failure by returning + # is_error=True is describing this attempt, not a stable answer — the + # upstream 503 or bad gateway it is reporting is exactly the kind of + # thing that clears on retry. Caching it would pin the failure in place + # for the full TTL and stop the tool from ever being retried. + if tool_result.is_error: + return tool_result + cacheable_tool_result: CacheableToolResult = CacheableToolResult.wrap( value=tool_result ) diff --git a/tests/server/middleware/test_caching.py b/tests/server/middleware/test_caching.py index c2d229468..fec5424c1 100644 --- a/tests/server/middleware/test_caching.py +++ b/tests/server/middleware/test_caching.py @@ -658,6 +658,51 @@ class TestCacheableToolResult: assert cached_tool_result.is_error is True +class TestErrorResultsAreNotCached: + """Regression tests for issue #4395: an error result was cached for the full + TTL, so a transient failure permanently shadowed the tool until it expired.""" + + async def test_error_result_is_not_cached(self): + mcp = FastMCP("ErrorCachingTestServer") + mcp.add_middleware(ResponseCachingMiddleware(cache_storage=MemoryStore())) + + call_count = 0 + + @mcp.tool + def flakey() -> ToolResult: + nonlocal call_count + call_count += 1 + if call_count == 1: + return ToolResult("upstream 503", is_error=True) + return ToolResult("recovered") + + async with Client(mcp) as client: + first = await client.call_tool("flakey", {}, raise_on_error=False) + assert first.is_error is True + + # The tool must actually run again rather than replay the error. + second = await client.call_tool("flakey", {}, raise_on_error=False) + assert second.is_error is False + assert call_count == 2 + + async def test_successful_result_is_still_cached(self): + mcp = FastMCP("SuccessCachingTestServer") + mcp.add_middleware(ResponseCachingMiddleware(cache_storage=MemoryStore())) + + call_count = 0 + + @mcp.tool + def stable() -> str: + nonlocal call_count + call_count += 1 + return "ok" + + async with Client(mcp) as client: + await client.call_tool("stable", {}) + await client.call_tool("stable", {}) + assert call_count == 1 + + class TestCachingWithImportedServerPrefixes: """Test that caching preserves prefixes from imported servers. From b9b7ea691461c7a4779374f8f0c82c67941efb2e Mon Sep 17 00:00:00 2001 From: nate nowack <153965+jlowin@users.noreply.github.com> Date: Wed, 5 Aug 2026 09:39:35 -0500 Subject: [PATCH 37/53] Declare run-claude extra allowed tools input (#4740) Generated with Codex --- .github/actions/run-claude/action.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/actions/run-claude/action.yml b/.github/actions/run-claude/action.yml index 66f4bb286..f97598d66 100644 --- a/.github/actions/run-claude/action.yml +++ b/.github/actions/run-claude/action.yml @@ -37,6 +37,11 @@ inputs: required: false default: "" + extra-allowed-tools: + description: "Additional comma-separated tools to append to allowed-tools" + required: false + default: "" + model: description: "Model to use for Claude" required: false From 2c2f98691f3e0b21ed6cc98ad4823d44fa93c5e8 Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Wed, 5 Aug 2026 10:53:15 -0400 Subject: [PATCH 38/53] Docs: mirror v3.4.6 release notes (#4764) --- docs/changelog.mdx | 16 ++++++++++++++++ docs/updates.mdx | 10 ++++++++++ 2 files changed, 26 insertions(+) diff --git a/docs/changelog.mdx b/docs/changelog.mdx index de17a3f8e..0f7efeb17 100644 --- a/docs/changelog.mdx +++ b/docs/changelog.mdx @@ -5,6 +5,22 @@ rss: true tag: NEW --- +<Update label="v3.4.6" description="2026-08-05"> + +**[v3.4.6: Trust, but Proxy](https://github.com/PrefectHQ/fastmcp/releases/tag/v3.4.6)** + +FastMCP 3.4.6 backports trusted-proxy support for SSRF-protected OAuth metadata and JWKS fetches. Deployments can now route these requests through a mandated corporate proxy while preserving custom CA certificates; FastMCP refuses the fetch when no proxy is configured instead of risking an unprotected direct request. + +### Fixes 🐞 +* Backport #4412 to 3.x: support trusted SSRF proxies by [@jlowin](https://github.com/jlowin) in [#4755](https://github.com/PrefectHQ/fastmcp/pull/4755) + +### Docs 📚 +* Docs: add v3.4.6 changelog entries by [@jlowin](https://github.com/jlowin) in [#4761](https://github.com/PrefectHQ/fastmcp/pull/4761) + +**Full Changelog**: [v3.4.5...v3.4.6](https://github.com/PrefectHQ/fastmcp/compare/v3.4.5...v3.4.6) + +</Update> + <Update label="v4.0.0b1" description="2026-07-28"> **[v4.0.0b1: Fourgone Conclusion](https://github.com/PrefectHQ/fastmcp/releases/tag/v4.0.0b1)** diff --git a/docs/updates.mdx b/docs/updates.mdx index e930cb36c..26e83c917 100644 --- a/docs/updates.mdx +++ b/docs/updates.mdx @@ -5,6 +5,16 @@ icon: "sparkles" tag: NEW --- +<Update label="FastMCP 3.4.6" description="August 5, 2026" tags={["Releases"]}> +<Card +title="FastMCP v3.4.6: Trust, but Proxy" +href="https://github.com/PrefectHQ/fastmcp/releases/tag/v3.4.6" +cta="Read the release notes" +> +FastMCP 3.4.6 adds trusted-proxy support for SSRF-protected OAuth metadata and JWKS fetches on the 3.x line. Deployments can route these requests through a mandated corporate proxy while preserving custom CA certificates, and FastMCP refuses the fetch when no proxy is configured instead of risking an unprotected direct request. +</Card> +</Update> + <Update label="FastMCP 4.0.0b1" description="July 28, 2026" tags={["Releases"]}> <Card title="FastMCP v4.0.0b1: Fourgone Conclusion" From e4d8ca648a0e8fcf60f958fa4979df61dae4c5c0 Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Wed, 5 Aug 2026 10:56:13 -0400 Subject: [PATCH 39/53] Avoid loading MCP and CLI stacks during lightweight imports (#4763) --- fastmcp_slim/fastmcp/__init__.py | 15 +------ fastmcp_slim/fastmcp/_compat.py | 2 +- fastmcp_slim/fastmcp/_warnings.py | 10 +++++ fastmcp_slim/fastmcp/exceptions.py | 11 ++--- .../fastmcp/server/mixins/transport.py | 5 ++- fastmcp_slim/fastmcp/utilities/logging.py | 26 +++++++---- tests/server/http/test_startup_imports.py | 45 +++++++++++++++++++ tests/test_compat.py | 5 +++ tests/utilities/test_logging.py | 18 +++++++- 9 files changed, 104 insertions(+), 33 deletions(-) create mode 100644 fastmcp_slim/fastmcp/_warnings.py diff --git a/fastmcp_slim/fastmcp/__init__.py b/fastmcp_slim/fastmcp/__init__.py index 170ffb0f4..9d64f128e 100644 --- a/fastmcp_slim/fastmcp/__init__.py +++ b/fastmcp_slim/fastmcp/__init__.py @@ -6,15 +6,13 @@ from importlib.metadata import PackageNotFoundError, version as _version from typing import TYPE_CHECKING from fastmcp import _install_hints +from fastmcp._warnings import FastMCPDeprecationWarning from fastmcp.settings import Settings from fastmcp.utilities.logging import configure_logging as _configure_logging if TYPE_CHECKING: from fastmcp.client import Client as Client from fastmcp.apps.app import FastMCPApp as FastMCPApp - from fastmcp.exceptions import ( - FastMCPDeprecationWarning as FastMCPDeprecationWarning, - ) from fastmcp.server.context import Context as Context from fastmcp.server.server import FastMCP as FastMCP @@ -39,12 +37,7 @@ except PackageNotFoundError: __version__ = _version("fastmcp") if settings.deprecation_warnings: - try: - from fastmcp.exceptions import FastMCPDeprecationWarning - except ImportError: - pass - else: - warnings.simplefilter("default", FastMCPDeprecationWarning) + warnings.simplefilter("default", FastMCPDeprecationWarning) # --- Lazy imports for performance (see #3292) --- @@ -81,10 +74,6 @@ def __getattr__(name: str) -> object: raise ImportError(_install_hints.APP_SUPPORT) from exc return FastMCPApp - if name == "FastMCPDeprecationWarning": - from fastmcp.exceptions import FastMCPDeprecationWarning - - return FastMCPDeprecationWarning if name == "client": try: return importlib.import_module("fastmcp.client") diff --git a/fastmcp_slim/fastmcp/_compat.py b/fastmcp_slim/fastmcp/_compat.py index 402b8c238..cefc67b49 100644 --- a/fastmcp_slim/fastmcp/_compat.py +++ b/fastmcp_slim/fastmcp/_compat.py @@ -33,7 +33,7 @@ import warnings import mcp_types -from fastmcp.exceptions import FastMCPDeprecationWarning +from fastmcp._warnings import FastMCPDeprecationWarning # Map each SDK model class to the camelCase -> snake_case field reads we bridge. # Limited to fields FastMCP users actually read (docs boundary inventory). diff --git a/fastmcp_slim/fastmcp/_warnings.py b/fastmcp_slim/fastmcp/_warnings.py new file mode 100644 index 000000000..c63b97a5d --- /dev/null +++ b/fastmcp_slim/fastmcp/_warnings.py @@ -0,0 +1,10 @@ +"""Warning types that can be imported without loading FastMCP's exception stack.""" + + +class FastMCPDeprecationWarning(DeprecationWarning): + """Deprecation warning for FastMCP APIs. + + Subclass of DeprecationWarning so that standard warning filters + still apply, but FastMCP can selectively enable its own warnings + without affecting other libraries in the process. + """ diff --git a/fastmcp_slim/fastmcp/exceptions.py b/fastmcp_slim/fastmcp/exceptions.py index 4042f80b7..3fa255acd 100644 --- a/fastmcp_slim/fastmcp/exceptions.py +++ b/fastmcp_slim/fastmcp/exceptions.py @@ -5,6 +5,8 @@ from typing import Any from mcp_types import INTERNAL_ERROR, INVALID_PARAMS, ErrorData +from fastmcp import _warnings + try: from mcp import MCPError except ImportError: @@ -30,14 +32,7 @@ except ImportError: # see the migration notes. McpError = MCPError - -class FastMCPDeprecationWarning(DeprecationWarning): - """Deprecation warning for FastMCP APIs. - - Subclass of DeprecationWarning so that standard warning filters - still apply, but FastMCP can selectively enable its own warnings - without affecting other libraries in the process. - """ +FastMCPDeprecationWarning = _warnings.FastMCPDeprecationWarning class FastMCPError(Exception): diff --git a/fastmcp_slim/fastmcp/server/mixins/transport.py b/fastmcp_slim/fastmcp/server/mixins/transport.py index 13bef1ced..b26e02d18 100644 --- a/fastmcp_slim/fastmcp/server/mixins/transport.py +++ b/fastmcp_slim/fastmcp/server/mixins/transport.py @@ -28,7 +28,6 @@ from fastmcp.server.http import ( from fastmcp.server.providers.base import Provider from fastmcp.server.providers.fastmcp_provider import FastMCPProvider from fastmcp.server.providers.wrapped_provider import _WrappedProvider -from fastmcp.utilities.cli import log_server_banner from fastmcp.utilities.logging import get_logger, temporary_log_level if TYPE_CHECKING: @@ -230,6 +229,8 @@ class TransportMixin: # Display server banner if show_banner: + from fastmcp.utilities.cli import log_server_banner + log_server_banner(server=self) token = set_transport("stdio") @@ -337,6 +338,8 @@ class TransportMixin: # Display server banner if show_banner: + from fastmcp.utilities.cli import log_server_banner + log_server_banner(server=self) uvicorn_config_from_user = uvicorn_config or {} diff --git a/fastmcp_slim/fastmcp/utilities/logging.py b/fastmcp_slim/fastmcp/utilities/logging.py index d39d6ef4a..eebf4eda8 100644 --- a/fastmcp_slim/fastmcp/utilities/logging.py +++ b/fastmcp_slim/fastmcp/utilities/logging.py @@ -1,7 +1,9 @@ """Logging utilities for FastMCP.""" import contextlib +import importlib.util import logging +from pathlib import Path from typing import Any, Literal, cast from rich.console import Console @@ -11,6 +13,17 @@ from typing_extensions import override import fastmcp +def _get_package_path(package: str) -> str | None: + """Return a package directory without importing the package.""" + try: + spec = importlib.util.find_spec(package) + except ImportError: + return None + if spec is None or spec.origin is None: + return None + return str(Path(spec.origin).parent) + + def get_logger(name: str) -> logging.Logger: """Get a logger nested under FastMCP namespace. @@ -83,14 +96,11 @@ def configure_logging( # no path or level name to maximize width available for the traceback # suppress framework frames and limit the number of frames to 3 - import pydantic - - try: - import mcp - except ImportError: - tracebacks_suppress = [fastmcp, pydantic] - else: - tracebacks_suppress = [fastmcp, mcp, pydantic] + tracebacks_suppress = [ + package_path + for package in ("fastmcp", "mcp", "pydantic") + if (package_path := _get_package_path(package)) is not None + ] # Build traceback kwargs with defaults that can be overridden traceback_kwargs = { diff --git a/tests/server/http/test_startup_imports.py b/tests/server/http/test_startup_imports.py index 52af15fc1..addcd99b7 100644 --- a/tests/server/http/test_startup_imports.py +++ b/tests/server/http/test_startup_imports.py @@ -9,6 +9,51 @@ import textwrap import pytest +@pytest.mark.subprocess_heavy +def test_root_import_does_not_load_mcp_sdk() -> None: + script = textwrap.dedent( + """ + import sys + + import fastmcp + + assert fastmcp.settings is not None + assert "mcp" not in sys.modules + assert "fastmcp.exceptions" not in sys.modules + """ + ) + + result = subprocess.run( + [sys.executable, "-c", script], + capture_output=True, + text=True, + ) + + assert result.returncode == 0, result.stderr + + +@pytest.mark.subprocess_heavy +def test_server_import_does_not_load_cli() -> None: + script = textwrap.dedent( + """ + import sys + + from fastmcp import FastMCP + + assert FastMCP is not None + assert "fastmcp.utilities.cli" not in sys.modules + """ + ) + + result = subprocess.run( + [sys.executable, "-c", script], + capture_output=True, + text=True, + ) + + assert result.returncode == 0, result.stderr + + @pytest.mark.subprocess_heavy def test_default_http_app_does_not_load_opt_in_integrations() -> None: script = textwrap.dedent( diff --git a/tests/test_compat.py b/tests/test_compat.py index ff5356a64..a44991f02 100644 --- a/tests/test_compat.py +++ b/tests/test_compat.py @@ -10,6 +10,7 @@ from mcp import MCPError as SDKMCPError import fastmcp import fastmcp._compat as _compat from fastmcp import Client, FastMCP +from fastmcp import FastMCPDeprecationWarning as PublicWarning from fastmcp.client.transports import FastMCPTransport from fastmcp.exceptions import FastMCPDeprecationWarning, MCPError, McpError @@ -30,6 +31,10 @@ def _reset_warn_once() -> None: _compat.install() +def test_deprecation_warning_is_same_from_public_imports() -> None: + assert PublicWarning is FastMCPDeprecationWarning + + @pytest.fixture(autouse=True) def fresh_shims(): _reset_warn_once() diff --git a/tests/utilities/test_logging.py b/tests/utilities/test_logging.py index d15b9c355..c74b6c436 100644 --- a/tests/utilities/test_logging.py +++ b/tests/utilities/test_logging.py @@ -1,4 +1,7 @@ import logging +from pathlib import Path + +from rich.logging import RichHandler import fastmcp from fastmcp.utilities.logging import configure_logging, get_logger @@ -42,6 +45,19 @@ def test_configure_logging_with_traceback_kwargs(): assert len(logger.handlers) == 2 # One for normal logs, one for tracebacks +def test_configure_logging_suppresses_framework_package_paths(): + configure_logging(enable_rich_tracebacks=True) + + traceback_handler = logging.getLogger("fastmcp").handlers[-1] + assert isinstance(traceback_handler, RichHandler) + suppressed_packages = { + Path(path).name + for path in traceback_handler.tracebacks_suppress + if isinstance(path, str) + } + assert {"fastmcp", "mcp", "pydantic"} <= suppressed_packages + + def test_configure_logging_traceback_defaults_can_be_overridden(): """Test that default traceback settings can be overridden by kwargs.""" configure_logging( @@ -91,8 +107,6 @@ def test_configure_logging_with_rich_enabled(): # Should have two handlers when rich logging is enabled (normal + traceback) assert len(logger.handlers) == 2 # Both should be RichHandler instances - from rich.logging import RichHandler - assert all(isinstance(h, RichHandler) for h in logger.handlers) finally: fastmcp.settings.enable_rich_logging = original_enable_rich From 8661193411ec0ed16bcf33513e7e5d59b6cab3e3 Mon Sep 17 00:00:00 2001 From: nate nowack <153965+jlowin@users.noreply.github.com> Date: Wed, 5 Aug 2026 10:15:51 -0500 Subject: [PATCH 40/53] Quote run-claude allowed tools argument (#4741) Generated with Codex --- .github/actions/run-claude/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/actions/run-claude/action.yml b/.github/actions/run-claude/action.yml index f97598d66..b79131462 100644 --- a/.github/actions/run-claude/action.yml +++ b/.github/actions/run-claude/action.yml @@ -93,7 +93,7 @@ runs: track_progress: ${{ inputs.track-progress }} prompt: ${{ inputs.prompt }} claude_args: | - ${{ (inputs.allowed-tools != '' || inputs.extra-allowed-tools != '') && format('--allowedTools {0}{1}', inputs.allowed-tools, inputs.extra-allowed-tools != '' && format(',{0}', inputs.extra-allowed-tools) || '') || '' }} + ${{ (inputs.allowed-tools != '' || inputs.extra-allowed-tools != '') && format('--allowedTools ''{0}{1}''', inputs.allowed-tools, inputs.extra-allowed-tools != '' && format(',{0}', inputs.extra-allowed-tools) || '') || '' }} ${{ inputs.mcp-servers != '' && format('--mcp-config ''{0}''', inputs.mcp-servers) || '' }} --model ${{ inputs.model }} settings: | From 959daf232157a5e4dd76e0b82326029c7513cb85 Mon Sep 17 00:00:00 2001 From: Jake Kaplan <40362401+jakekaplan@users.noreply.github.com> Date: Wed, 5 Aug 2026 15:46:10 -0400 Subject: [PATCH 41/53] Sanitize forwarded request metadata where the proxy copies it (#4770) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Separate proxy protocol policy from client construction 🤖 Generated with OpenAI Codex * Strip connection-owned request metadata at the proxy backend boundary Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Sanitize forwarded request metadata where the proxy copies it Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Forward hop-safe request metadata for proxied resources, templates, and prompts Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> --- docs/servers/providers/proxy.mdx | 4 + .../fastmcp/server/providers/proxy.py | 75 +++++-- .../providers/proxy/test_proxy_client.py | 18 +- .../proxy/test_proxy_request_meta.py | 191 ++++++++++++++++++ .../providers/proxy/test_proxy_server.py | 19 +- .../proxy/test_stateful_proxy_client.py | 9 +- 6 files changed, 263 insertions(+), 53 deletions(-) create mode 100644 tests/server/providers/proxy/test_proxy_request_meta.py diff --git a/docs/servers/providers/proxy.mdx b/docs/servers/providers/proxy.mdx index 2be18cefd..df09b1c80 100644 --- a/docs/servers/providers/proxy.mdx +++ b/docs/servers/providers/proxy.mdx @@ -240,6 +240,10 @@ proxy = create_proxy( A modern client here reaches both `weather` and `calendar` on modern sessions, so a guard tool on either one round-trips end to end. An explicit `mode` pins every backend in the configuration, the same way it pins a single one. +### Request Metadata + +Request `_meta` follows the same connection boundary. Progress tokens, tracing, task state, and application or vendor metadata pass through the proxy to the backend. The connection-owned keys — protocol version, client identity, and client capabilities — never copy from the frontend connection: a modern backend session stamps its own negotiated values, and a handshake-era backend receives none. This holds even when the two connections negotiate different eras, such as a modern client reaching a handshake-only backend through an explicit `mode`. + ## Configuration-Based Proxies <VersionBadge version="2.4.0" /> diff --git a/fastmcp_slim/fastmcp/server/providers/proxy.py b/fastmcp_slim/fastmcp/server/providers/proxy.py index 9b1cd4a69..a304eacc3 100644 --- a/fastmcp_slim/fastmcp/server/providers/proxy.py +++ b/fastmcp_slim/fastmcp/server/providers/proxy.py @@ -130,6 +130,50 @@ def _proxy_upstream_error(error: Exception) -> MCPError: ) +# Request `_meta` keys that describe one negotiated MCP connection. They never +# cross the proxy: a modern backend session stamps its own negotiated values on +# every request, and a handshake-era backend must not receive them at all. +_CONNECTION_META_KEYS = frozenset( + { + mcp_types.PROTOCOL_VERSION_META_KEY, + mcp_types.CLIENT_INFO_META_KEY, + mcp_types.CLIENT_CAPABILITIES_META_KEY, + } +) + + +def _forwardable_request_meta(ctx: Context | None) -> dict[str, Any] | None: + """Frontend request metadata that may cross onto the backend connection. + + This is the proxy's one sanctioned read of the inbound request's `_meta`: + progress tokens, tracing, task, and application metadata pass through, + while connection-owned keys (`_CONNECTION_META_KEYS`) are dropped because + they describe the frontend connection, not the backend one. + """ + request_context = ctx.request_context if ctx is not None else None + if request_context is None or not request_context.meta: + return None + forwarded = { + key: value + for key, value in request_context.meta.items() + if key not in _CONNECTION_META_KEYS + } + return forwarded or None + + +def _session_request_meta( + meta: dict[str, Any] | None, +) -> mcp_types.RequestParamsMeta | None: + """Adapt forwardable metadata for a direct backend-session call. + + Direct session calls bypass the high-level client mixins, so trace context + is injected here, matching what the mixins do on the legacy client paths. + """ + return cast( + "mcp_types.RequestParamsMeta | None", inject_trace_context(meta) or None + ) + + async def _relay_read_resource( client: Client, uri: str, ctx: Context | None ) -> ( @@ -143,15 +187,15 @@ async def _relay_read_resource( to forward, instead of the high-level client trying to answer it here — the proxy has no back-channel to the real user, so driving it fails outright. The inbound request's continuation state travels down so the backend guard - sees the client's answers on its own `ctx.input_responses`. Trace context - still propagates: the SDK's JSON-RPC dispatcher injects it on every outgoing - request (SEP-414), below whichever client layer issued the call. + sees the client's answers on its own `ctx.input_responses`. """ + meta = _forwardable_request_meta(ctx) if client.protocol_version not in MODERN_PROTOCOL_VERSIONS: - return await client.read_resource(uri) + return await client.read_resource(uri, meta=meta) result = await client._await_with_session_monitoring( client.session.read_resource( uri, + meta=_session_request_meta(meta), input_responses=ctx.input_responses if ctx else None, request_state=ctx.request_state if ctx else None, allow_input_required=True, @@ -311,15 +355,11 @@ class ProxyTool(Tool): async with client: ctx = context or get_context() _stash_proxy_request_context(client, ctx) - # Forward the inbound request's `_meta` block (trace context, - # version, etc.) to the backend. In SDK v2 the request context - # exposes the lifted `_meta` dict directly; task submission is a - # first-class params field rather than context state, so there - # is no separate task-metadata injection here. - req_ctx = ctx.request_context - meta: dict[str, Any] | None = ( - dict(req_ctx.meta) if req_ctx is not None and req_ctx.meta else None - ) + # Forward the inbound request's hop-safe `_meta` (trace + # context, progress token, etc.) to the backend. Task + # submission is a first-class params field rather than context + # state, so there is no separate task-metadata injection here. + meta = _forwardable_request_meta(ctx) if client.protocol_version in MODERN_PROTOCOL_VERSIONS: # Modern backend: call the session directly (not @@ -330,10 +370,7 @@ class ProxyTool(Tool): # round. Forward the inbound request's continuation state # down so the backend guard tool sees the client's answers # on its own `ctx.input_responses` / `ctx.request_state`. - request_meta = cast( - "mcp_types.RequestParamsMeta | None", - inject_trace_context(meta) or None, - ) + request_meta = _session_request_meta(meta) # SEP-2243: a modern backend rejects a `tools/call` whose # `x-mcp-header` argument is not mirrored into an `Mcp-Param-*` # header. The SDK client emits those headers only for tools it @@ -704,6 +741,7 @@ class ProxyPrompt(Prompt): ctx = get_context() async with client: _stash_proxy_request_context(client, ctx) + meta = _forwardable_request_meta(ctx) if client.protocol_version in MODERN_PROTOCOL_VERSIONS: # See `_relay_read_resource`: surface a backend guard's ask # instead of trying to answer it inside the proxy. @@ -711,6 +749,7 @@ class ProxyPrompt(Prompt): client.session.get_prompt( backend_name, arguments, + meta=_session_request_meta(meta), input_responses=ctx.input_responses if ctx else None, request_state=ctx.request_state if ctx else None, allow_input_required=True, @@ -720,7 +759,7 @@ class ProxyPrompt(Prompt): return InputRequiredPromptResult(raw) result = raw else: - result = await client.get_prompt(backend_name, arguments) + result = await client.get_prompt(backend_name, arguments, meta=meta) # Convert GetPromptResult to PromptResult, preserving meta from result # (not the static prompt meta which includes fastmcp tags) # Convert PromptMessages to Messages diff --git a/tests/server/providers/proxy/test_proxy_client.py b/tests/server/providers/proxy/test_proxy_client.py index caa4b9075..53b37129a 100644 --- a/tests/server/providers/proxy/test_proxy_client.py +++ b/tests/server/providers/proxy/test_proxy_client.py @@ -150,19 +150,11 @@ async def proxy_server(fastmcp_server: FastMCP): `ProxyClient(fastmcp_server)` defaults to `mode="legacy"` (see `TestProxyClientEraDefault` above — a directly-constructed `ProxyClient` always pins the handshake era, independent of `create_proxy`'s era - mirroring). Every test below that forwards a tool call through this - fixture (not just a listing) needs its front `Client` pinned to - `mode="legacy"` too, for either or both of two reasons: - - - The test's subject is itself a handshake-only feature (roots / sampling - / elicitation push, logging, progress): the modern era has no - back-channel for server-initiated requests at all, so these forwarding - paths cannot exist there. - - Even for subjects that work on both eras, a modern front's request - `_meta` carries reserved modern-envelope keys that `ProxyTool.run`'s - legacy-backend path forwards verbatim onto this legacy-locked backend - session, which the backend server then rejects as a protocol - violation. + mirroring). Tests below that exercise a handshake-only feature (roots / + sampling / elicitation push, logging, progress) pin their front `Client` + to `mode="legacy"` too: the modern era has no back-channel for + server-initiated requests at all, so these forwarding paths cannot exist + there. """ return create_proxy(ProxyClient(fastmcp_server)) diff --git a/tests/server/providers/proxy/test_proxy_request_meta.py b/tests/server/providers/proxy/test_proxy_request_meta.py new file mode 100644 index 000000000..4776eedb5 --- /dev/null +++ b/tests/server/providers/proxy/test_proxy_request_meta.py @@ -0,0 +1,191 @@ +"""Request `_meta` ownership at the proxy's backend connection boundary. + +Protocol version, client identity, and client capabilities describe one +negotiated MCP connection. The proxy must never copy them from its frontend +connection onto its backend connection: a modern backend session stamps its +own values, and a handshake-era backend must not receive them at all. +Progress, tracing, task, and application metadata pass through untouched. +""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + +import mcp_types +import pytest +from mcp.client.extension import ClientExtension +from mcp_types.version import MODERN_PROTOCOL_VERSIONS + +from fastmcp import Client, Context, FastMCP +from fastmcp.server.providers.proxy import FastMCPProxy, ProxyClient + +FRONT_EXTENSION_ID = "example.com/frontend" +FRONT_INFO = mcp_types.Implementation(name="frontend-client", version="1.0") +BACKEND_INFO = mcp_types.Implementation(name="proxy-backend", version="1.0") +RESERVED_META_KEYS = { + mcp_types.PROTOCOL_VERSION_META_KEY, + mcp_types.CLIENT_INFO_META_KEY, + mcp_types.CLIENT_CAPABILITIES_META_KEY, +} + + +@dataclass +class _RecordedRequest: + protocol_version: str + meta: dict[str, Any] + + +class _FrontendExtension(ClientExtension): + identifier = FRONT_EXTENSION_ID + + def settings(self) -> dict[str, Any]: + return {"frontend": True} + + +def _recording_backend(seen: dict[str, _RecordedRequest]) -> FastMCP: + backend = FastMCP("metadata-backend") + + def record(operation: str, ctx: Context) -> None: + request_context = ctx.request_context + assert request_context is not None + seen[operation] = _RecordedRequest( + protocol_version=request_context.protocol_version, + meta=dict(request_context.meta or {}), + ) + + @backend.tool + def inspect_tool(ctx: Context) -> str: + record("tool", ctx) + return "ok" + + @backend.resource("data://metadata") + def inspect_resource(ctx: Context) -> str: + record("resource", ctx) + return "ok" + + @backend.resource("data://items/{item_id}") + def inspect_template(item_id: str, ctx: Context) -> str: + record("template", ctx) + return "ok" + + @backend.prompt + def inspect_prompt(ctx: Context) -> str: + record("prompt", ctx) + return "ok" + + return backend + + +def _proxy( + backend: FastMCP, *, backend_mode: str, client_class: type[Client] +) -> FastMCPProxy: + return FastMCPProxy( + client_factory=lambda: client_class( + backend, + mode=backend_mode, + client_info=BACKEND_INFO, + ) + ) + + +def _assert_backend_connection_meta(record: _RecordedRequest, modern: bool) -> None: + """The backend request carries the backend connection's own envelope. + + On a handshake-era backend the reserved keys are absent. On a modern + backend they hold the backend session's negotiated version and the proxy + client's identity and capabilities — never the frontend client's. + """ + meta = record.meta + if not modern: + assert RESERVED_META_KEYS.isdisjoint(meta) + return + + assert meta[mcp_types.PROTOCOL_VERSION_META_KEY] == record.protocol_version + assert meta[mcp_types.CLIENT_INFO_META_KEY] == BACKEND_INFO.model_dump( + by_alias=True, mode="json", exclude_none=True + ) + capabilities = meta[mcp_types.CLIENT_CAPABILITIES_META_KEY] + assert FRONT_EXTENSION_ID not in capabilities.get("extensions", {}) + + +# Every allowed ClientFactoryT shape must be hop-safe, not just ProxyClient: +# a plain Client backend runs the SDK's stock ClientSession rather than the +# proxy's session class, so it exercises the copy-site sanitization alone. +@pytest.mark.parametrize("client_class", [ProxyClient, Client]) +@pytest.mark.parametrize( + ("front_mode", "backend_mode", "backend_is_modern"), + [ + ("auto", "auto", True), + ("auto", "legacy", False), + ("legacy", "auto", True), + ("legacy", "legacy", False), + ], +) +async def test_forwarded_tool_meta_stays_hop_safe( + front_mode: str, + backend_mode: str, + backend_is_modern: bool, + client_class: type[Client], +): + seen: dict[str, _RecordedRequest] = {} + proxy = _proxy( + _recording_backend(seen), backend_mode=backend_mode, client_class=client_class + ) + + async with Client( + proxy, + mode=front_mode, + client_info=FRONT_INFO, + extensions=[_FrontendExtension()], + ) as client: + await client.call_tool( + "inspect_tool", + meta={ + "progressToken": "front-progress", + "example.com/vendor": {"request": "kept"}, + }, + ) + + record = seen["tool"] + assert (record.protocol_version in MODERN_PROTOCOL_VERSIONS) is backend_is_modern + assert isinstance(record.meta["progressToken"], str | int) + assert record.meta["example.com/vendor"] == {"request": "kept"} + _assert_backend_connection_meta(record, backend_is_modern) + + +@pytest.mark.parametrize("client_class", [ProxyClient, Client]) +@pytest.mark.parametrize( + ("backend_mode", "backend_is_modern"), + [("auto", True), ("legacy", False)], +) +@pytest.mark.parametrize("operation", ["resource", "template", "prompt"]) +async def test_non_tool_requests_forward_hop_safe_metadata( + operation: str, + backend_mode: str, + backend_is_modern: bool, + client_class: type[Client], +): + seen: dict[str, _RecordedRequest] = {} + proxy = _proxy( + _recording_backend(seen), backend_mode=backend_mode, client_class=client_class + ) + meta = {"example.com/vendor": {"operation": operation}} + + async with Client( + proxy, + mode="auto", + client_info=FRONT_INFO, + extensions=[_FrontendExtension()], + ) as client: + if operation == "resource": + await client.read_resource("data://metadata", meta=meta) + elif operation == "template": + await client.read_resource("data://items/42", meta=meta) + else: + await client.get_prompt("inspect_prompt", meta=meta) + + record = seen[operation] + assert (record.protocol_version in MODERN_PROTOCOL_VERSIONS) is backend_is_modern + assert record.meta["example.com/vendor"] == {"operation": operation} + _assert_backend_connection_meta(record, backend_is_modern) diff --git a/tests/server/providers/proxy/test_proxy_server.py b/tests/server/providers/proxy/test_proxy_server.py index 8971b5ba0..fe489d56f 100644 --- a/tests/server/providers/proxy/test_proxy_server.py +++ b/tests/server/providers/proxy/test_proxy_server.py @@ -172,13 +172,7 @@ async def proxy_server(fastmcp_server): raw `FastMCP`/URL/etc.) means `create_proxy` reuses that client as-is instead of building one through the era-mirroring factory — so this backend stays pinned to `ProxyClient`'s own default of `mode="legacy"` - regardless of what era the front client negotiates. A test that actually - forwards a tool *call* through this fixture (not just a listing) needs - its own front `Client` pinned to `mode="legacy"` too: otherwise a modern - front's request `_meta` carries the reserved modern-envelope keys, which - `ProxyTool.run`'s legacy-backend path forwards verbatim onto this - legacy-locked backend session, and the backend server rejects it as a - protocol violation. + regardless of what era the front client negotiates. """ return create_proxy(ProxyClient(transport=FastMCPTransport(fastmcp_server))) @@ -1250,11 +1244,7 @@ class TestProxyOutputSchemaEnforcement: # This proxy's backend is built via `ProxyProvider(lambda: ProxyClient(...))` # directly rather than through `create_proxy`'s era-mirroring factory, so it # stays pinned to `ProxyClient`'s own default of `mode="legacy"` regardless - # of the front era (see the `proxy_server` fixture docstring above for the - # full explanation). Pin the end client to match: a modern front's request - # `_meta` carries reserved modern-envelope keys that `ProxyTool.run`'s - # legacy-backend path forwards verbatim, and this legacy-locked backend - # session rejects them as a protocol violation. + # of the front era (see the `proxy_server` fixture docstring above). client = Client(server, mode="legacy") client._transport_options = TransportOptions( session_class=_ForwardingClientSession @@ -1428,10 +1418,7 @@ class TestProxyForwardingAppliesToEveryBackendClient: # era. A multi-server config instead mounts a router with a # StatefulProxyClient per configured server leg — an already-constructed # ProxyClient subclass, same as the `proxy_server` fixture above, pinned - # to `mode="legacy"` regardless of the front. Callers with that backend - # shape must pin the end client to legacy too, for the reason explained - # there (a modern front's request `_meta` gets forwarded verbatim onto a - # legacy-locked backend session and rejected as a protocol violation). + # to `mode="legacy"` regardless of the front. client = Client(server, mode=mode) client._transport_options = TransportOptions( session_class=_ForwardingClientSession diff --git a/tests/server/providers/proxy/test_stateful_proxy_client.py b/tests/server/providers/proxy/test_stateful_proxy_client.py index 1b64afd2d..0bf832440 100644 --- a/tests/server/providers/proxy/test_stateful_proxy_client.py +++ b/tests/server/providers/proxy/test_stateful_proxy_client.py @@ -62,12 +62,9 @@ async def stateful_proxy_server(fastmcp_server: FastMCP): # `mode="legacy"` default for a directly-constructed instance (see # `TestProxyClientEraDefault` in test_proxy_client.py) — this backend isn't # built through `create_proxy`'s era-mirroring factory, so it stays pinned - # regardless of the front era. Every test below that forwards a real tool - # call through this fixture pins its front `Client` to `mode="legacy"` too: - # otherwise a modern front's request `_meta` carries reserved - # modern-envelope keys that `ProxyTool.run`'s legacy-backend path forwards - # verbatim, and this legacy-locked backend session rejects them as a - # protocol violation. + # regardless of the front era. Tests of handshake-only forwarding pin their + # front `Client` to `mode="legacy"` too: those server-initiated + # interactions do not exist on modern connections. client = StatefulProxyClient(transport=FastMCPTransport(fastmcp_server)) return FastMCPProxy(client_factory=client.new_stateful) From 875e8e18bd41a81a6614183d7b982a4c79bb1be2 Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Wed, 5 Aug 2026 18:09:16 -0400 Subject: [PATCH 42/53] Preserve legacy httpx compatibility without importing it (#4766) --- .../upgrading/from-fastmcp-3.mdx | 2 +- .../server/auth/oauth_proxy/upstream.py | 3 +- .../server/providers/openapi/README.md | 6 +- .../server/providers/openapi/components.py | 101 +++++------ .../server/providers/openapi/provider.py | 22 ++- fastmcp_slim/fastmcp/server/server.py | 55 ++---- fastmcp_slim/fastmcp/utilities/exceptions.py | 52 +++--- .../fastmcp/utilities/openapi/README.md | 14 +- .../providers/openapi/test_comprehensive.py | 7 - .../openapi/test_legacy_client_compat.py | 168 ++++++------------ tests/server/test_legacy_httpx_errors.py | 33 ++++ tests/test_no_legacy_httpx.py | 41 ++++- 12 files changed, 256 insertions(+), 248 deletions(-) create mode 100644 tests/server/test_legacy_httpx_errors.py diff --git a/docs/getting-started/upgrading/from-fastmcp-3.mdx b/docs/getting-started/upgrading/from-fastmcp-3.mdx index 37ff33f62..1cd484680 100644 --- a/docs/getting-started/upgrading/from-fastmcp-3.mdx +++ b/docs/getting-started/upgrading/from-fastmcp-3.mdx @@ -209,7 +209,7 @@ transport = StreamableHttpTransport( ) ``` -The `client` you pass to `FastMCP.from_openapi(client=...)` (and `OpenAPIProvider(client=...)`) is now type-hinted `httpx2.AsyncClient`. FastMCP does not gate on the type, so an existing `httpx.AsyncClient` keeps working at runtime via duck-typing this release — but switching it to `httpx2.AsyncClient` clears the type hint and is the supported path going forward. HTTP made inside your own tools is entirely yours and is unaffected either way. +The `client` you pass to `FastMCP.from_openapi(client=...)` (and `OpenAPIProvider(client=...)`) should now be an `httpx2.AsyncClient`. Existing `httpx.AsyncClient` instances remain temporarily accepted via duck typing, but emit a `FastMCPDeprecationWarning` and will be rejected in a future release. HTTP made inside your own tools is entirely yours and is unaffected. **The subtlest break is exception handlers, and no type checker will catch it.** `httpx` very likely remains installed in your environment (the Anthropic, OpenAI, and Google SDKs all depend on it), so code that catches old-httpx exceptions around FastMCP calls still imports and still type-checks — it just never matches, because FastMCP now raises `httpx2` exceptions. The handler silently becomes dead code: diff --git a/fastmcp_slim/fastmcp/server/auth/oauth_proxy/upstream.py b/fastmcp_slim/fastmcp/server/auth/oauth_proxy/upstream.py index 8739aa7a3..bed40f670 100644 --- a/fastmcp_slim/fastmcp/server/auth/oauth_proxy/upstream.py +++ b/fastmcp_slim/fastmcp/server/auth/oauth_proxy/upstream.py @@ -43,8 +43,7 @@ class AsyncOAuth2Client: Drop-in replacement for the slice of authlib's `AsyncOAuth2Client` that `OAuthProxy` uses. Subclasses of `OAuthProxy` that override `_create_upstream_oauth_client` may return any object with the same - `fetch_token`/`refresh_token`/`client_secret`/`aclose` surface (including - an authlib client, if legacy httpx is installed in their environment). + `fetch_token`/`refresh_token`/`client_secret`/`aclose` surface. """ def __init__( diff --git a/fastmcp_slim/fastmcp/server/providers/openapi/README.md b/fastmcp_slim/fastmcp/server/providers/openapi/README.md index 8c5e890c4..8b55d8753 100644 --- a/fastmcp_slim/fastmcp/server/providers/openapi/README.md +++ b/fastmcp_slim/fastmcp/server/providers/openapi/README.md @@ -53,7 +53,7 @@ The main server class orchestrates the stateless request building approach: ```python class FastMCPOpenAPI(FastMCP): - def __init__(self, openapi_spec: dict, client: httpx.AsyncClient, **kwargs): + def __init__(self, openapi_spec: dict, client: httpx2.AsyncClient, **kwargs): # 1. Parse OpenAPI spec to HTTP routes with pre-calculated schemas self._routes = parse_openapi_to_http_routes(openapi_spec) @@ -92,7 +92,7 @@ OpenAPI Spec → HTTPRoute with Pre-calculated Fields → RequestDirector → HT 2. **RequestDirector Setup**: openapi-core Spec initialized for request building 3. **Component Creation**: Create components with RequestDirector reference 4. **Request Building**: RequestDirector builds HTTP request from flat parameters -5. **Request Execution**: Execute request with httpx client +5. **Request Execution**: Execute request with httpx2 client 6. **Response Processing**: Return structured MCP response ## Key Features @@ -263,4 +263,4 @@ logging.getLogger("fastmcp.server.openapi_new").setLevel(logging.DEBUG) - `/utilities/openapi_new/README.md` - Utility implementation details - `/server/openapi/README.md` - Legacy implementation reference - `/tests/server/openapi_new/` - Comprehensive test suite -- Project documentation on OpenAPI integration patterns \ No newline at end of file +- Project documentation on OpenAPI integration patterns diff --git a/fastmcp_slim/fastmcp/server/providers/openapi/components.py b/fastmcp_slim/fastmcp/server/providers/openapi/components.py index 3cb36abcf..a6b88538e 100644 --- a/fastmcp_slim/fastmcp/server/providers/openapi/components.py +++ b/fastmcp_slim/fastmcp/server/providers/openapi/components.py @@ -4,7 +4,7 @@ from __future__ import annotations import json import re -from typing import TYPE_CHECKING, Any, cast +from typing import TYPE_CHECKING, Any import httpx2 from mcp_types import ToolAnnotations @@ -18,11 +18,7 @@ from fastmcp.resources import ( ) from fastmcp.server.dependencies import get_http_headers from fastmcp.tools.base import Tool, ToolResult -from fastmcp.utilities.exceptions import ( - HTTP_STATUS_ERRORS, - REQUEST_ERRORS, - TIMEOUT_ERRORS, -) +from fastmcp.utilities.exceptions import is_request_error, is_timeout_error from fastmcp.utilities.logging import get_logger from fastmcp.utilities.openapi import HTTPRoute from fastmcp.utilities.openapi.director import RequestDirector @@ -63,6 +59,36 @@ logger = get_logger(__name__) _DEFAULT_MIME_TYPE = "application/json" +def _raise_for_status(response: httpx2.Response) -> None: + """Raise an OpenAPI-formatted error without relying on client exception types.""" + if 200 <= response.status_code < 300: + return + + error_message = f"HTTP error {response.status_code}: {response.reason_phrase}" + try: + error_data = response.json() + error_message += f" - {error_data}" + except (json.JSONDecodeError, ValueError): + if response.text: + error_message += f" - {response.text}" + raise ValueError(error_message) + + +async def _send_request( + client: httpx2.AsyncClient, + request: httpx2.Request, +) -> httpx2.Response: + """Send a request while preserving transitional legacy-client errors.""" + try: + return await client.send(request) + except Exception as exc: + if is_timeout_error(exc): + raise ValueError(f"HTTP request timed out ({type(exc).__name__})") from exc + if is_request_error(exc): + raise ValueError(f"Request error ({type(exc).__name__}): {exc!s}") from exc + raise + + def _extract_mime_type_from_route(route: HTTPRoute) -> str: """Extract the primary MIME type from an HTTPRoute's response definitions. @@ -176,12 +202,8 @@ class OpenAPITool(Tool): base_url = str(self._client.base_url) or "http://localhost" directed_request = self._director.build(self._route, arguments, base_url) - # Rebuild through the user's client so the request object comes - # from whichever httpx library the client belongs to (a legacy - # httpx.AsyncClient cannot send an httpx2.Request). Primitive - # values (str/bytes/tuples) cross that boundary safely; client - # default headers merge in with directed headers taking priority, - # matching the previous manual merge. + # Rebuild through the configured client so its default headers are + # merged with the directed headers taking priority. request = self._client.build_request( method=directed_request.method, url=str(directed_request.url.copy_with(query=None)), @@ -210,8 +232,8 @@ class OpenAPITool(Tool): f"run - sending request; headers: {_redact_headers(request.headers)}" ) - response = await self._client.send(request) - response.raise_for_status() + response = await _send_request(self._client, request) + _raise_for_status(response) # Try to parse as JSON first try: @@ -238,25 +260,11 @@ class OpenAPITool(Tool): except json.JSONDecodeError: return ToolResult(content=response.text) - except HTTP_STATUS_ERRORS as e: - status_error = cast("httpx2.HTTPStatusError", e) - error_message = ( - f"HTTP error {status_error.response.status_code}: " - f"{status_error.response.reason_phrase}" - ) - try: - error_data = status_error.response.json() - error_message += f" - {error_data}" - except (json.JSONDecodeError, ValueError): - if status_error.response.text: - error_message += f" - {status_error.response.text}" - raise ValueError(error_message) from e + except httpx2.TimeoutException as exc: + raise ValueError(f"HTTP request timed out ({type(exc).__name__})") from exc - except TIMEOUT_ERRORS as e: - raise ValueError(f"HTTP request timed out ({type(e).__name__})") from e - - except REQUEST_ERRORS as e: - raise ValueError(f"Request error ({type(e).__name__}): {e!s}") from e + except httpx2.RequestError as exc: + raise ValueError(f"Request error ({type(exc).__name__}): {exc!s}") from exc class OpenAPIResource(Resource): @@ -298,8 +306,7 @@ class OpenAPIResource(Resource): directed_request = self._director.build( self._route, self._arguments, base_url ) - # Primitive values only: a legacy httpx.AsyncClient cannot accept - # httpx2 URL/QueryParams/Headers objects. + # Build through the configured client so its defaults are applied. request = self._client.build_request( method=directed_request.method, url=str(directed_request.url.copy_with(query=None)), @@ -314,8 +321,8 @@ class OpenAPIResource(Resource): if mcp_headers: request.headers.update(mcp_headers) - response = await self._client.send(request) - response.raise_for_status() + response = await _send_request(self._client, request) + _raise_for_status(response) content_type = response.headers.get("content-type", "").lower() @@ -343,25 +350,11 @@ class OpenAPIResource(Resource): ] ) - except HTTP_STATUS_ERRORS as e: - status_error = cast("httpx2.HTTPStatusError", e) - error_message = ( - f"HTTP error {status_error.response.status_code}: " - f"{status_error.response.reason_phrase}" - ) - try: - error_data = status_error.response.json() - error_message += f" - {error_data}" - except (json.JSONDecodeError, ValueError): - if status_error.response.text: - error_message += f" - {status_error.response.text}" - raise ValueError(error_message) from e + except httpx2.TimeoutException as exc: + raise ValueError(f"HTTP request timed out ({type(exc).__name__})") from exc - except TIMEOUT_ERRORS as e: - raise ValueError(f"HTTP request timed out ({type(e).__name__})") from e - - except REQUEST_ERRORS as e: - raise ValueError(f"Request error ({type(e).__name__}): {e!s}") from e + except httpx2.RequestError as exc: + raise ValueError(f"Request error ({type(exc).__name__}): {exc!s}") from exc def _path_argument_name(route: HTTPRoute, parameter_name: str) -> str: diff --git a/fastmcp_slim/fastmcp/server/providers/openapi/provider.py b/fastmcp_slim/fastmcp/server/providers/openapi/provider.py index 4048479a0..c16f14034 100644 --- a/fastmcp_slim/fastmcp/server/providers/openapi/provider.py +++ b/fastmcp_slim/fastmcp/server/providers/openapi/provider.py @@ -2,6 +2,7 @@ from __future__ import annotations +import warnings from collections import Counter from collections.abc import AsyncIterator, Sequence from contextlib import asynccontextmanager @@ -10,6 +11,7 @@ from typing import Any, Literal, cast import httpx2 from jsonschema_path import SchemaPath +from fastmcp._warnings import FastMCPDeprecationWarning from fastmcp.prompts import Prompt from fastmcp.resources import Resource, ResourceTemplate from fastmcp.server.providers.base import Provider @@ -48,6 +50,14 @@ logger = get_logger(__name__) DEFAULT_TIMEOUT: float = 30.0 +def _is_legacy_httpx_client(client: object) -> bool: + """Detect a legacy httpx client without importing the legacy package.""" + return any( + cls.__module__.partition(".")[0] == "httpx" and cls.__name__ == "AsyncClient" + for cls in type(client).__mro__ + ) + + class OpenAPIProvider(Provider): """Provider that creates MCP components from an OpenAPI specification. @@ -84,10 +94,12 @@ class OpenAPIProvider(Provider): Args: openapi_spec: OpenAPI schema as a dictionary - client: Optional httpx AsyncClient for making HTTP requests. + client: Optional httpx2 AsyncClient for making HTTP requests. If not provided, a default client is created using the first server URL from the OpenAPI spec with a 30-second timeout. To customize timeout or other settings, pass your own client. + Legacy httpx clients are temporarily accepted with a deprecation + warning. route_maps: Optional list of RouteMap objects defining route mappings route_map_fn: Optional callable for advanced route type mapping mcp_component_fn: Optional callable for component customization @@ -103,6 +115,14 @@ class OpenAPIProvider(Provider): self._owns_client = client is None if client is None: client = self._create_default_client(openapi_spec) + elif _is_legacy_httpx_client(client): + warnings.warn( + "Passing an httpx.AsyncClient to OpenAPIProvider is deprecated " + "and will be removed in a future release. Pass an " + "httpx2.AsyncClient instead.", + FastMCPDeprecationWarning, + stacklevel=2, + ) self._client = client self._mcp_component_fn = mcp_component_fn self._validate_output = validate_output diff --git a/fastmcp_slim/fastmcp/server/server.py b/fastmcp_slim/fastmcp/server/server.py index a25a78712..de3cba77b 100644 --- a/fastmcp_slim/fastmcp/server/server.py +++ b/fastmcp_slim/fastmcp/server/server.py @@ -88,7 +88,7 @@ from fastmcp.tools.base import Tool, ToolResult from fastmcp.tools.function_tool import FunctionTool from fastmcp.tools.tool_transform import ToolTransformConfig from fastmcp.utilities.components import FastMCPComponent, _coerce_version -from fastmcp.utilities.exceptions import HTTP_STATUS_ERRORS, TIMEOUT_ERRORS +from fastmcp.utilities.exceptions import get_http_status_code, is_timeout_error from fastmcp.utilities.logging import get_logger from fastmcp.utilities.tasks import TaskConfig from fastmcp.utilities.types import AnyFunction, FastMCPBaseModel, NotSet, NotSetT @@ -112,11 +112,6 @@ if TYPE_CHECKING: logger = get_logger(__name__) -# Both-library catch tuples for user-supplied code that may still raise legacy -# httpx exceptions; see fastmcp.utilities.exceptions for the defensive import. -_ACTIONABLE_HTTP_STATUS_ERRORS = HTTP_STATUS_ERRORS -_ACTIONABLE_TIMEOUT_ERRORS = TIMEOUT_ERRORS - def _version_request_meta( version: VersionSpec | None, @@ -1546,15 +1541,11 @@ class FastMCP( logger.exception(f"Error calling tool {name!r}") # Handle actionable errors that should reach the LLM # even when masking is enabled - if isinstance(e, _ACTIONABLE_HTTP_STATUS_ERRORS): - if ( - cast("httpx2.HTTPStatusError", e).response.status_code - == 429 - ): - raise ToolError( - "Rate limited by upstream API, please retry later" - ) from e - if isinstance(e, _ACTIONABLE_TIMEOUT_ERRORS): + if get_http_status_code(e) == 429: + raise ToolError( + "Rate limited by upstream API, please retry later" + ) from e + if is_timeout_error(e): raise ToolError( "Upstream request timed out, please retry" ) from e @@ -1649,15 +1640,11 @@ class FastMCP( except Exception as e: logger.exception(f"Error reading resource {uri!r}") # Handle actionable errors that should reach the LLM - if isinstance(e, _ACTIONABLE_HTTP_STATUS_ERRORS): - if ( - cast("httpx2.HTTPStatusError", e).response.status_code - == 429 - ): - raise ResourceError( - "Rate limited by upstream API, please retry later" - ) from e - if isinstance(e, _ACTIONABLE_TIMEOUT_ERRORS): + if get_http_status_code(e) == 429: + raise ResourceError( + "Rate limited by upstream API, please retry later" + ) from e + if is_timeout_error(e): raise ResourceError( "Upstream request timed out, please retry" ) from e @@ -1712,15 +1699,11 @@ class FastMCP( except Exception as e: logger.exception(f"Error reading resource {uri!r}") # Handle actionable errors that should reach the LLM - if isinstance(e, _ACTIONABLE_HTTP_STATUS_ERRORS): - if ( - cast("httpx2.HTTPStatusError", e).response.status_code - == 429 - ): - raise ResourceError( - "Rate limited by upstream API, please retry later" - ) from e - if isinstance(e, _ACTIONABLE_TIMEOUT_ERRORS): + if get_http_status_code(e) == 429: + raise ResourceError( + "Rate limited by upstream API, please retry later" + ) from e + if is_timeout_error(e): raise ResourceError( "Upstream request timed out, please retry" ) from e @@ -2412,10 +2395,10 @@ class FastMCP( Args: openapi_spec: OpenAPI schema as a dictionary client: Optional httpx2 AsyncClient for making HTTP requests. - An httpx (v1) AsyncClient is also accepted and works via - duck-typing. If not provided, a default client is created - using the first + If not provided, a default client is created using the first server URL from the OpenAPI spec with a 30-second timeout. + Legacy httpx clients are temporarily accepted with a deprecation + warning. name: Name for the MCP server route_maps: Optional list of RouteMap objects defining route mappings route_map_fn: Optional callable for advanced route type mapping diff --git a/fastmcp_slim/fastmcp/utilities/exceptions.py b/fastmcp_slim/fastmcp/utilities/exceptions.py index f9166a2b6..97cea8f29 100644 --- a/fastmcp_slim/fastmcp/utilities/exceptions.py +++ b/fastmcp_slim/fastmcp/utilities/exceptions.py @@ -7,30 +7,42 @@ from mcp import MCPError import fastmcp -# FastMCP uses httpx2 internally, but user-supplied code (tools, resources, and -# clients handed to the OpenAPI integration) may still raise exceptions from the -# legacy httpx package. These catch tuples include both families when httpx is -# installed, so user errors keep their specific handling without making httpx a -# FastMCP dependency. The two libraries' exception hierarchies match name-for-name. -try: - import httpx - HTTP_STATUS_ERRORS: tuple[type[BaseException], ...] = ( - httpx2.HTTPStatusError, - httpx.HTTPStatusError, +def _is_legacy_httpx_exception(exc: BaseException, exception_type: str) -> bool: + """Check a legacy-httpx exception without importing the legacy package.""" + return any( + cls.__module__.partition(".")[0] == "httpx" and cls.__name__ == exception_type + for cls in type(exc).__mro__ ) - TIMEOUT_ERRORS: tuple[type[BaseException], ...] = ( - httpx2.TimeoutException, - httpx.TimeoutException, + + +def is_http_status_error(exc: BaseException) -> bool: + """Return whether an exception is an httpx2 or legacy-httpx status error.""" + return isinstance(exc, httpx2.HTTPStatusError) or _is_legacy_httpx_exception( + exc, "HTTPStatusError" ) - REQUEST_ERRORS: tuple[type[BaseException], ...] = ( - httpx2.RequestError, - httpx.RequestError, + + +def get_http_status_code(exc: BaseException) -> int | None: + """Return the response status code from a recognized HTTP status error.""" + if not is_http_status_error(exc): + return None + status_code = getattr(getattr(exc, "response", None), "status_code", None) + return status_code if isinstance(status_code, int) else None + + +def is_timeout_error(exc: BaseException) -> bool: + """Return whether an exception is an httpx2 or legacy-httpx timeout.""" + return isinstance(exc, httpx2.TimeoutException) or _is_legacy_httpx_exception( + exc, "TimeoutException" + ) + + +def is_request_error(exc: BaseException) -> bool: + """Return whether an exception is an httpx2 or legacy-httpx request error.""" + return isinstance(exc, httpx2.RequestError) or _is_legacy_httpx_exception( + exc, "RequestError" ) -except ImportError: - HTTP_STATUS_ERRORS = (httpx2.HTTPStatusError,) - TIMEOUT_ERRORS = (httpx2.TimeoutException,) - REQUEST_ERRORS = (httpx2.RequestError,) def iter_exc(group: BaseExceptionGroup): diff --git a/fastmcp_slim/fastmcp/utilities/openapi/README.md b/fastmcp_slim/fastmcp/utilities/openapi/README.md index 2f2a5f45f..c5e478e19 100644 --- a/fastmcp_slim/fastmcp/utilities/openapi/README.md +++ b/fastmcp_slim/fastmcp/utilities/openapi/README.md @@ -47,7 +47,7 @@ OpenAPI Spec → Parser → HTTPRoute with Pre-calculated Fields → RequestDire ### Request Processing ``` -MCP Tool Call → RequestDirector.build() → httpx.Request → HTTP Response → Structured Output +MCP Tool Call → RequestDirector.build() → httpx2.Request → HTTP Response → Structured Output ``` 1. **Tool Invocation**: FastMCP receives tool call with parameters @@ -103,14 +103,14 @@ All components use the same RequestDirector approach: ### Basic Server Setup ```python -import httpx +import httpx2 from fastmcp.server.openapi import FastMCPOpenAPI # OpenAPI spec (can be loaded from file/URL) openapi_spec = {...} # Create HTTP client -async with httpx.AsyncClient() as client: +async with httpx2.AsyncClient() as client: # Create server with stateless request building server = FastMCPOpenAPI( openapi_spec=openapi_spec, @@ -134,8 +134,8 @@ director = RequestDirector(spec) # Build HTTP request request = director.build(route, flat_arguments, base_url) -# Execute with httpx -async with httpx.AsyncClient() as client: +# Execute with httpx2 +async with httpx2.AsyncClient() as client: response = await client.send(request) ``` @@ -206,6 +206,6 @@ Tests are located in `/tests/server/openapi/`: ## Dependencies - `openapi-core` - OpenAPI specification processing and validation -- `httpx` - HTTP client library +- `httpx2` - HTTP client library - `pydantic` - Data validation and serialization -- `urllib.parse` - URL building and manipulation \ No newline at end of file +- `urllib.parse` - URL building and manipulation diff --git a/tests/server/providers/openapi/test_comprehensive.py b/tests/server/providers/openapi/test_comprehensive.py index d5b8ceffd..ace2a03a5 100644 --- a/tests/server/providers/openapi/test_comprehensive.py +++ b/tests/server/providers/openapi/test_comprehensive.py @@ -653,13 +653,6 @@ class TestOpenAPIComprehensive: mock_response.json.return_value = {"code": 404, "message": "User not found"} mock_response.text = json.dumps({"code": 404, "message": "User not found"}) - # Configure raise_for_status to raise HTTPStatusError - def raise_for_status(): - raise httpx2.HTTPStatusError( - "404 Not Found", request=Mock(), response=mock_response - ) - - mock_response.raise_for_status = raise_for_status mock_client.send = AsyncMock(return_value=mock_response) server = create_openapi_server( diff --git a/tests/server/providers/openapi/test_legacy_client_compat.py b/tests/server/providers/openapi/test_legacy_client_compat.py index 707be823d..3b0361ad8 100644 --- a/tests/server/providers/openapi/test_legacy_client_compat.py +++ b/tests/server/providers/openapi/test_legacy_client_compat.py @@ -1,20 +1,9 @@ -"""Legacy-httpx client compatibility for the OpenAPI integration. - -The upgrade guide promises that an existing legacy ``httpx.AsyncClient`` passed -to ``OpenAPIProvider``/``FastMCP.from_openapi`` keeps working via duck-typing. -That requires two things of the OpenAPI request path: requests must be built -through the user's own client (``build_request``), and errors raised by that -client — which are legacy-httpx exceptions, not httpx2 — must still receive the -integration's specific error formatting rather than surfacing as generic -failures. -""" +"""Deprecation bridge for legacy-httpx OpenAPI clients.""" import pytest -from fastmcp import FastMCP -from fastmcp.client import Client +from fastmcp import Client, FastMCP, FastMCPDeprecationWarning from fastmcp.exceptions import ToolError -from fastmcp.server.providers.openapi import OpenAPIProvider httpx = pytest.importorskip("httpx", reason="legacy httpx not installed") @@ -26,7 +15,6 @@ SPEC = { "/items": { "get": { "operationId": "list_items", - "summary": "List items", "responses": { "200": { "description": "Items", @@ -46,121 +34,77 @@ SPEC = { } }, } - }, + } }, } -def _legacy_client(handler) -> "httpx.AsyncClient": - transport = httpx.MockTransport(handler) - return httpx.AsyncClient(transport=transport, base_url="https://api.example.com") - - -def _server(client) -> FastMCP: - mcp = FastMCP("Legacy Client Server") - mcp.add_provider(OpenAPIProvider(openapi_spec=SPEC, client=client)) - return mcp - - -async def test_tool_call_with_legacy_client_succeeds(): - """A legacy httpx.AsyncClient drives an OpenAPI tool end-to-end.""" - +async def test_legacy_client_warns_and_remains_usable() -> None: def handler(request: "httpx.Request") -> "httpx.Response": - assert isinstance(request, httpx.Request) return httpx.Response(200, json={"items": ["a", "b"]}) - async with _legacy_client(handler) as client: - async with Client(_server(client)) as mcp_client: + transport = httpx.MockTransport(handler) + async with httpx.AsyncClient( + transport=transport, + base_url="https://api.example.com", + ) as client: + with pytest.warns( + FastMCPDeprecationWarning, + match="httpx.AsyncClient.*deprecated", + ): + server = FastMCP.from_openapi(SPEC, client=client) + + async with Client(server) as mcp_client: result = await mcp_client.call_tool("list_items", {}) - assert result.structured_content == {"items": ["a", "b"]} + + assert result.structured_content == {"items": ["a", "b"]} -async def test_tool_http_error_keeps_openapi_formatting_with_legacy_client(): - """A legacy client's HTTP error still gets the integration's message format. - - The handler raises legacy ``httpx.HTTPStatusError``; the catch tuples must - recognize it so the error carries the formatted status + body rather than a - generic failure. - """ - +async def test_legacy_client_preserves_http_error_details() -> None: def handler(request: "httpx.Request") -> "httpx.Response": - return httpx.Response(500, json={"detail": "boom"}) + return httpx.Response(404, json={"detail": "items not found"}) - async with _legacy_client(handler) as client: - async with Client(_server(client)) as mcp_client: - with pytest.raises(ToolError, match="HTTP error 500") as excinfo: - await mcp_client.call_tool("list_items", {}) - assert "boom" in str(excinfo.value) + transport = httpx.MockTransport(handler) + async with httpx.AsyncClient( + transport=transport, + base_url="https://api.example.com", + ) as client: + with pytest.warns(FastMCPDeprecationWarning): + server = FastMCP.from_openapi(SPEC, client=client) - -async def test_tool_request_error_keeps_openapi_formatting_with_legacy_client(): - """A legacy client's transport error maps to the formatted request error.""" - - def handler(request: "httpx.Request") -> "httpx.Response": - raise httpx.ConnectError("connection refused") - - async with _legacy_client(handler) as client: - async with Client(_server(client)) as mcp_client: - with pytest.raises(ToolError, match="Request error"): + async with Client(server) as mcp_client: + with pytest.raises(ToolError, match="HTTP error 404") as exc_info: await mcp_client.call_tool("list_items", {}) + assert "items not found" in str(exc_info.value) -async def test_multipart_tool_call_with_legacy_client(): - """Multipart bodies must materialize and send through a legacy client too.""" - spec = { - "openapi": "3.0.0", - "info": {"title": "Upload API", "version": "1.0.0"}, - "servers": [{"url": "https://api.example.com"}], - "paths": { - "/upload": { - "post": { - "operationId": "upload_file", - "summary": "Upload a file", - "requestBody": { - "required": True, - "content": { - "multipart/form-data": { - "schema": { - "type": "object", - "properties": {"file": {"type": "string"}}, - } - } - }, - }, - "responses": { - "200": { - "description": "Uploaded", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": {"ok": {"type": "boolean"}}, - } - } - }, - } - }, - } - } - }, - } - received: dict[str, object] = {} +@pytest.mark.parametrize( + ("error_kind", "message"), + [ + ("timeout", "HTTP request timed out (ReadTimeout)"), + ("connect", "Request error (ConnectError)"), + ], +) +async def test_legacy_client_preserves_transport_error_details( + error_kind: str, + message: str, +) -> None: def handler(request: "httpx.Request") -> "httpx.Response": - received["content_type"] = request.headers.get("content-type", "") - received["body"] = request.read() - return httpx.Response(200, json={"ok": True}) + if error_kind == "timeout": + raise httpx.ReadTimeout("transport failed", request=request) + raise httpx.ConnectError("transport failed", request=request) - async with _legacy_client(handler) as client: - mcp = FastMCP("Legacy Multipart Server") - mcp.add_provider(OpenAPIProvider(openapi_spec=spec, client=client)) - async with Client(mcp) as mcp_client: - result = await mcp_client.call_tool("upload_file", {"file": "data"}) - assert result.structured_content == {"ok": True} + transport = httpx.MockTransport(handler) + async with httpx.AsyncClient( + transport=transport, + base_url="https://api.example.com", + ) as client: + with pytest.warns(FastMCPDeprecationWarning): + server = FastMCP.from_openapi(SPEC, client=client) - content_type = received["content_type"] - assert isinstance(content_type, str) - assert "multipart/form-data" in content_type - body = received["body"] - assert isinstance(body, bytes) - assert b"data" in body + async with Client(server) as mcp_client: + with pytest.raises(ToolError) as exc_info: + await mcp_client.call_tool("list_items", {}) + + assert message in str(exc_info.value) diff --git a/tests/server/test_legacy_httpx_errors.py b/tests/server/test_legacy_httpx_errors.py new file mode 100644 index 000000000..38f9434a0 --- /dev/null +++ b/tests/server/test_legacy_httpx_errors.py @@ -0,0 +1,33 @@ +"""Compatibility tests for legacy-httpx exceptions raised by user code.""" + +import pytest + +from fastmcp import FastMCP +from fastmcp.exceptions import ResourceError, ToolError + +httpx = pytest.importorskip("httpx", reason="legacy httpx not installed") + + +async def test_legacy_httpx_rate_limit_remains_actionable() -> None: + server = FastMCP("Legacy httpx errors", mask_error_details=True) + + @server.tool + def rate_limited() -> None: + request = httpx.Request("GET", "https://example.com") + response = httpx.Response(429, request=request) + raise httpx.HTTPStatusError("rate limited", request=request, response=response) + + with pytest.raises(ToolError, match="Rate limited by upstream API"): + await server.call_tool("rate_limited", {}) + + +async def test_legacy_httpx_resource_timeout_remains_actionable() -> None: + server = FastMCP("Legacy httpx errors", mask_error_details=True) + + @server.resource("resource://timed-out") + def timed_out() -> str: + request = httpx.Request("GET", "https://example.com") + raise httpx.ReadTimeout("timed out", request=request) + + with pytest.raises(ResourceError, match="Upstream request timed out"): + await server.read_resource("resource://timed-out") diff --git a/tests/test_no_legacy_httpx.py b/tests/test_no_legacy_httpx.py index 5cd439f87..24ee01366 100644 --- a/tests/test_no_legacy_httpx.py +++ b/tests/test_no_legacy_httpx.py @@ -6,11 +6,9 @@ masks clean-install regressions: an accidental ``import httpx`` (directly or via a third-party integration such as authlib's httpx client) passes CI but breaks any install without those extras. -This test simulates the clean install by running a subprocess that blocks -legacy httpx imports at the meta-path level, then imports the modules that -have historically regressed. The defensive user-compat shim in -``fastmcp.server.server`` catches ImportError by design and must keep working -when httpx is absent. +These tests simulate a clean install by blocking legacy httpx imports at the +meta-path level and verify that ordinary server startup leaves both legacy +packages unloaded. """ import subprocess @@ -45,6 +43,28 @@ _BLOCKER_SCRIPT = textwrap.dedent( """ ) +_STARTUP_SCRIPT = textwrap.dedent( + """ + import sys + + from fastmcp import FastMCP + + server = FastMCP("Legacy httpx import guard") + app = server.http_app(transport="http", stateless_http=True) + assert app is not None + + loaded = [ + name + for name in sys.modules + if name == "httpx" + or name.startswith("httpx.") + or name == "httpcore" + or name.startswith("httpcore.") + ] + assert not loaded, loaded + """ +) + @pytest.mark.subprocess_heavy def test_fastmcp_imports_without_legacy_httpx(): @@ -58,3 +78,14 @@ def test_fastmcp_imports_without_legacy_httpx(): f"Import failed with legacy httpx blocked:\n{result.stderr}" ) assert "OK" in result.stdout + + +@pytest.mark.subprocess_heavy +def test_default_http_app_does_not_load_legacy_httpx(): + result = subprocess.run( + [sys.executable, "-c", _STARTUP_SCRIPT], + capture_output=True, + text=True, + timeout=30, + ) + assert result.returncode == 0, result.stderr From 2bee9aeb58c83f1ce7dc2f66240d1ef60b1b2c48 Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Thu, 6 Aug 2026 09:34:11 -0400 Subject: [PATCH 43/53] Clarify review of closed contributor PRs (#4780) --- CLAUDE.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index 1a2cbddb0..79b040531 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -56,6 +56,8 @@ When modifying MCP functionality, changes typically need to be applied across al **Read `CONTRIBUTING.md` before opening issues or PRs.** It describes when PRs are appropriate, what we expect from enhancement proposals, and what we'll close without review. +**Review closed contributor PRs.** When reviewing an issue, inspect every associated non-maintainer PR, including closed PRs. External PRs may be closed as part of the issue-link and assignment workflow, so closure alone is not a negative signal. Read `CONTRIBUTING.md` and the PR timeline and comments to understand its status before evaluating it. + ### Git & CI - Prek hooks are required (run automatically on commits) From c8b88b3a3763c3ce76868b1e92fa80f417084f46 Mon Sep 17 00:00:00 2001 From: Yonatan <yonaigross@gmail.com> Date: Thu, 6 Aug 2026 16:35:49 +0300 Subject: [PATCH 44/53] fix(context): move elicit overload docs inside the stubs so mypy sees the chain (#4774) --- fastmcp_slim/fastmcp/server/context.py | 39 ++++++++++---------------- 1 file changed, 15 insertions(+), 24 deletions(-) diff --git a/fastmcp_slim/fastmcp/server/context.py b/fastmcp_slim/fastmcp/server/context.py index 3373e09da..0e3ccf9d5 100644 --- a/fastmcp_slim/fastmcp/server/context.py +++ b/fastmcp_slim/fastmcp/server/context.py @@ -962,9 +962,8 @@ class Context: *, response_title: str | None = None, response_description: str | None = None, - ) -> AcceptedElicitation[T] | DeclinedElicitation | CancelledElicitation: ... - - """The accepted elicitation will contain the response data""" + ) -> AcceptedElicitation[T] | DeclinedElicitation | CancelledElicitation: + """The accepted elicitation will contain the response data""" @overload async def elicit( @@ -974,10 +973,9 @@ class Context: *, response_title: str | None = None, response_description: str | None = None, - ) -> AcceptedElicitation[str] | DeclinedElicitation | CancelledElicitation: ... - - """When response_type is a list of strings, the accepted elicitation will - contain the selected string response""" + ) -> AcceptedElicitation[str] | DeclinedElicitation | CancelledElicitation: + """When response_type is a list of strings, the accepted elicitation will + contain the selected string response""" @overload async def elicit( @@ -987,10 +985,9 @@ class Context: *, response_title: str | None = None, response_description: str | None = None, - ) -> AcceptedElicitation[str] | DeclinedElicitation | CancelledElicitation: ... - - """When response_type is a dict mapping keys to title dicts, the accepted - elicitation will contain the selected key""" + ) -> AcceptedElicitation[str] | DeclinedElicitation | CancelledElicitation: + """When response_type is a dict mapping keys to title dicts, the accepted + elicitation will contain the selected key""" @overload async def elicit( @@ -1000,12 +997,9 @@ class Context: *, response_title: str | None = None, response_description: str | None = None, - ) -> ( - AcceptedElicitation[list[str]] | DeclinedElicitation | CancelledElicitation - ): ... - - """When response_type is a list containing a list of strings (multi-select), - the accepted elicitation will contain a list of selected strings""" + ) -> AcceptedElicitation[list[str]] | DeclinedElicitation | CancelledElicitation: + """When response_type is a list containing a list of strings (multi-select), + the accepted elicitation will contain a list of selected strings""" @overload async def elicit( @@ -1015,13 +1009,10 @@ class Context: *, response_title: str | None = None, response_description: str | None = None, - ) -> ( - AcceptedElicitation[list[str]] | DeclinedElicitation | CancelledElicitation - ): ... - - """When response_type is a list containing a dict mapping keys to title dicts - (multi-select with titles), the accepted elicitation will contain a list of - selected keys""" + ) -> AcceptedElicitation[list[str]] | DeclinedElicitation | CancelledElicitation: + """When response_type is a list containing a dict mapping keys to title dicts + (multi-select with titles), the accepted elicitation will contain a list of + selected keys""" async def elicit( self, From 6fb34e9383cd6b44bef606c06b493503c8354b61 Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Thu, 6 Aug 2026 11:15:38 -0400 Subject: [PATCH 45/53] Document MCP protocol support and conformance (#4781) --- docs/more/faq.mdx | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/docs/more/faq.mdx b/docs/more/faq.mdx index 1ac3c2c7d..5566e908f 100644 --- a/docs/more/faq.mdx +++ b/docs/more/faq.mdx @@ -22,16 +22,24 @@ The client probes `server/discover` and adopts the modern protocol when the serv ## What are the two protocol eras, and which one does my server speak? -Both. A FastMCP server serves every era from one deployment and one URL, and the SDK negotiates per connection — the client picks, not the server. +Both. A FastMCP 4 server supports the handshake revisions `2024-11-05`, `2025-03-26`, `2025-06-18`, and `2025-11-25`, plus the modern `2026-07-28` protocol. It serves all of them from one deployment and one URL, and the SDK negotiates per connection — the client picks, not the server. The *handshake* era (`2025-11-25` and earlier) opens each connection with `initialize` and holds a session, which gives the server a back-channel it can push requests down. The *modern* era (`2026-07-28`) is sessionless: the client learns what the server offers through `server/discover`, every request stands alone, and there is no back-channel. Inside a tool, `ctx.request_context.protocol_version` tells you which era the current call arrived on; on the client, `client.protocol_version` reports it after connecting. +A protocol version establishes the wire format, while capabilities describe which optional operations a particular server provides. The capabilities returned by `server/discover` or `initialize` are therefore the authoritative way for a client to determine what is available. + ## Can FastMCP 4 talk to older clients and servers? Yes, in both directions, with no configuration. A FastMCP 4 server answers a handshake-era client and a modern one from the same process: the old client sends `initialize` and gets a session id, the modern client discovers and stays stateless. A FastMCP 4 client is equally happy against an old server, because `mode="auto"` falls back to the handshake when discovery finds no modern peer. The client-side handlers for server-initiated capabilities are all still there too — passing `sampling_handler=` or `roots=` answers a legacy server's requests exactly as before, which is what a modern client needs in order to interoperate. See [client sampling](/clients/sampling) and [client roots](/clients/roots). +## How does FastMCP verify protocol conformance? + +FastMCP runs the [official MCP conformance suite](https://github.com/modelcontextprotocol/conformance) in CI against a pinned suite release. A failing scenario for a released capability that FastMCP advertises as supported is treated as a regression. + +The suite's `all` mode also exercises draft, pending, retired, and deliberately unsupported capabilities, so its raw pass count is broader than FastMCP's support contract. Known exceptions are recorded in [`expected-failures.yml`](https://github.com/PrefectHQ/fastmcp/blob/main/tests/conformance/expected-failures.yml) with their rationale, and new upstream scenarios arrive through deliberate suite-version updates rather than silently changing CI. + ## When should I pin `mode="legacy"`? Pin it when your code depends on the session the handshake creates: `client.ping()` and `transport.get_session_id()` have no modern equivalent, since a sessionless connection has neither a live back-channel to ping nor an id to hold. It is also the escape hatch when a server misbehaves under discovery or you need the classic `initialize` result object. From 75fb116e36807aa2edfa191974c8dcbca24927c0 Mon Sep 17 00:00:00 2001 From: nate nowack <thrast36@gmail.com> Date: Thu, 6 Aug 2026 13:09:37 -0500 Subject: [PATCH 46/53] Support EdDSA verification in JWTVerifier (#4752) --- docs/servers/auth/token-verification.mdx | 18 +- .../fastmcp/server/auth/providers/jwt.py | 54 ++++-- fastmcp_slim/pyproject.toml | 2 +- tests/server/auth/test_jwt_provider.py | 164 +++++++++++++++--- uv.lock | 2 +- 5 files changed, 195 insertions(+), 45 deletions(-) diff --git a/docs/servers/auth/token-verification.mdx b/docs/servers/auth/token-verification.mdx index b21e8b54d..9e55640ba 100644 --- a/docs/servers/auth/token-verification.mdx +++ b/docs/servers/auth/token-verification.mdx @@ -80,6 +80,19 @@ This configuration creates a server that validates JWTs issued by `auth.yourcomp The `issuer` parameter ensures tokens come from your trusted authentication system, while `audience` validation prevents tokens intended for other services from being accepted by your MCP server. +`JWTVerifier` accepts RSA (`RS*` and `PS*`), ECDSA (`ES*`), and Edwards-curve (`Ed25519` and `Ed448`) signatures from JWKS endpoints. Set `algorithm` when your issuer does not use the default `RS256`: + +```python +verifier = JWTVerifier( + jwks_uri="https://auth.yourcompany.com/.well-known/jwks.json", + issuer="https://auth.yourcompany.com", + audience="mcp-production-api", + algorithm="Ed25519", +) +``` + +The legacy `EdDSA` identifier is also accepted for compatibility with identity providers that have not yet adopted the fully specified identifiers from RFC 9864. + ### Symmetric Key Verification (HMAC) Symmetric key verification uses a shared secret for both signing and validation, making it ideal for internal microservices and trusted environments where the same secret can be securely distributed to both token issuers and validators. @@ -121,7 +134,7 @@ The parameter is named `public_key` for backwards compatibility, but when using ### Static Public Key Verification -Static public key verification works when you have a fixed RSA or ECDSA signing key and don't need automatic key rotation. This approach is primarily useful for development environments or controlled deployments where JWKS endpoints aren't available. +Static public key verification works when you have a fixed RSA, ECDSA, or EdDSA signing key and don't need automatic key rotation. This approach is primarily useful for development environments or controlled deployments where JWKS endpoints aren't available. ```python from fastmcp import FastMCP @@ -141,7 +154,7 @@ verifier = JWTVerifier( mcp = FastMCP(name="Protected API", auth=verifier) ``` -This configuration validates tokens using a specific RSA or ECDSA public key. The key must correspond to the private key used by your token issuer. While less flexible than JWKS endpoints, this approach can be useful in development environments or when testing with fixed keys. +This configuration validates tokens using a specific RSA, ECDSA, or EdDSA public key. The key must correspond to the private key used by your token issuer. While less flexible than JWKS endpoints, this approach can be useful in development environments or when testing with fixed keys. ## Opaque Token Verification Many authorization servers issue opaque tokens rather than self-contained JWTs. Opaque tokens are random strings that carry no information themselves - the authorization server maintains their state and validation requires querying the server. FastMCP supports opaque token validation through OAuth 2.0 Token Introspection (RFC 7662). @@ -425,4 +438,3 @@ mcp = FastMCP(name="Production API", auth=verifier) This keeps configuration out of your codebase while maintaining explicit setup. This approach enables the same codebase to run across development, staging, and production environments with different authentication requirements. Development might use static tokens while production uses JWT verification, all controlled through environment configuration. - diff --git a/fastmcp_slim/fastmcp/server/auth/providers/jwt.py b/fastmcp_slim/fastmcp/server/auth/providers/jwt.py index 64c8549cf..d991b2ab6 100644 --- a/fastmcp_slim/fastmcp/server/auth/providers/jwt.py +++ b/fastmcp_slim/fastmcp/server/auth/providers/jwt.py @@ -6,7 +6,7 @@ import contextlib import json import time from dataclasses import dataclass -from typing import Any, TypeAlias, cast +from typing import Any, Literal, TypeAlias, cast import httpx2 from cryptography.hazmat.primitives import serialization @@ -29,22 +29,30 @@ JWKKeyData: TypeAlias = dict[str, str | list[str]] SUPPORTED_JWS_HEADER_FIELDS = frozenset(JWS_HEADER_REGISTRY) -def _import_key_for_algorithm(key: str | bytes | JWKKeyData, algorithm: str): +def _key_type_for_algorithm(algorithm: str) -> Literal["oct", "RSA", "EC", "OKP"]: if algorithm.startswith("HS"): - return jwk.import_key(key, "oct") + return "oct" if algorithm.startswith(("RS", "PS")): - return jwk.import_key(key, "RSA") + return "RSA" if algorithm.startswith("ES"): - return jwk.import_key(key, "EC") + return "EC" + if algorithm in {"EdDSA", "Ed25519", "Ed448"}: + return "OKP" raise ValueError(f"Unsupported algorithm: {algorithm}.") +def _import_key_for_algorithm(key: str | bytes | JWKKeyData, algorithm: str): + return jwk.import_key(key, _key_type_for_algorithm(algorithm)) + + def _jwk_to_pem(key_data: JWKKeyData) -> str: key_type = key_data.get("kty") if key_type == "RSA": return jwk.import_key(key_data, "RSA").as_pem().decode("utf-8") if key_type == "EC": return jwk.import_key(key_data, "EC").as_pem().decode("utf-8") + if key_type == "OKP": + return jwk.import_key(key_data, "OKP").as_pem().decode("utf-8") raise ValueError(f"Unsupported JWK key type: {key_type!r}") @@ -72,6 +80,8 @@ class JWKData(TypedDict, total=False): alg: str # Algorithm (e.g., "RS256") n: str # Modulus (for RSA keys) e: str # Exponent (for RSA keys) + crv: str # Curve name (for EC and OKP keys) + x: str # Public key coordinate (for EC and OKP keys) x5c: list[str] # X.509 certificate chain (for JWKs) x5t: str # X.509 certificate thumbprint (for JWKs) @@ -194,10 +204,11 @@ def _looks_like_pem_public_key(key: str | bytes) -> bool: class JWTVerifier(TokenVerifier): """ - JWT token verifier supporting both asymmetric (RSA/ECDSA) and symmetric (HMAC) algorithms. + JWT token verifier supporting asymmetric (RSA/ECDSA/EdDSA) and symmetric (HMAC) algorithms. This verifier validates JWT tokens using various signing algorithms: - - **Asymmetric algorithms** (RS256/384/512, ES256/384/512, PS256/384/512): + - **Asymmetric algorithms** (RS256/384/512, ES256/384/512, PS256/384/512, + Ed25519, Ed448, and legacy EdDSA): Uses public/private key pairs. Ideal for external clients and services where only the authorization server has the private key. - **Symmetric algorithms** (HS256/384/512): Uses a shared secret for both @@ -232,7 +243,7 @@ class JWTVerifier(TokenVerifier): jwks_uri: URI to fetch a JSON Web Key Set; used when verifying tokens with remote JWKS. issuer: Expected issuer claim value or list of allowed issuer values. audience: Expected audience claim value or list of allowed audience values. - algorithm: JWT signing algorithm to accept (default: "RS256"). Supported: HS256/384/512, RS256/384/512, ES256/384/512, PS256/384/512. + algorithm: JWT signing algorithm to accept (default: "RS256"). Supported: HS256/384/512, RS256/384/512, ES256/384/512, PS256/384/512, Ed25519, Ed448, and legacy EdDSA. required_scopes: Scopes that must be present in validated tokens. base_url: Base URL passed to the parent TokenVerifier. ssrf_safe: If True, JWKS fetches use SSRF protection (HTTPS-only, @@ -275,6 +286,9 @@ class JWTVerifier(TokenVerifier): "PS256", "PS384", "PS512", + "EdDSA", + "Ed25519", + "Ed448", }: raise ValueError(f"Unsupported algorithm: {algorithm}.") @@ -347,19 +361,31 @@ class JWTVerifier(TokenVerifier): try: jwks_data = await self._fetch_jwks() - # Cache all usable keys. A key that cannot be converted (e.g. an - # unsupported kty like OKP/Ed25519) is skipped rather than failing - # the whole set — per RFC 7517 §5, clients should ignore JWKs they - # don't understand. Otherwise one exotic key published by the - # authorization server would reject every token, including ones - # signed by supported keys in the same set (#4515). + # Cache all usable keys. A key that cannot be converted is skipped + # rather than failing the whole set — per RFC 7517 §5, clients + # should ignore JWKs they don't understand. Otherwise one exotic + # key published by the authorization server would reject every + # token, including ones signed by supported keys in the same set + # (#4515). self._jwks_cache = {} skipped_kids: set[str] = set() + expected_key_type = _key_type_for_algorithm(self.algorithm) for key_data in jwks_data.get("keys", []): if not isinstance(key_data, dict): self.logger.debug("Skipping non-object JWKS entry: %r", key_data) continue key_kid = key_data.get("kid") + if key_data.get("kty") != expected_key_type: + self.logger.debug( + "Skipping JWKS key %r: key type %r is incompatible " + "with algorithm %s", + key_kid, + key_data.get("kty"), + self.algorithm, + ) + if key_kid: + skipped_kids.add(key_kid) + continue try: public_key = _jwk_to_pem(key_data) except (JoseError, TypeError, KeyError, ValueError) as e: diff --git a/fastmcp_slim/pyproject.toml b/fastmcp_slim/pyproject.toml index b009efd42..6d56f3e93 100644 --- a/fastmcp_slim/pyproject.toml +++ b/fastmcp_slim/pyproject.toml @@ -96,7 +96,7 @@ server = [ "griffelib>=2.0.0", "jsonref>=1.1.0", "jsonschema-path>=0.3.4", - "joserfc>=1.1.0", + "joserfc>=1.5.0", "openapi-pydantic>=0.5.1", "packaging>=24.0", "py-key-value-aio[filetree,keyring,memory]>=0.4.4,<0.5.0", diff --git a/tests/server/auth/test_jwt_provider.py b/tests/server/auth/test_jwt_provider.py index 7a39eb9df..20007f3e5 100644 --- a/tests/server/auth/test_jwt_provider.py +++ b/tests/server/auth/test_jwt_provider.py @@ -4,6 +4,9 @@ from typing import Any, cast from unittest.mock import MagicMock, patch import pytest +from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.primitives.asymmetric.ed448 import Ed448PrivateKey +from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey from joserfc import jwk as jose_jwk from joserfc import jwt from joserfc.jws import JWSRegistry @@ -81,6 +84,49 @@ class SymmetricKeyHelper: return token +def create_okp_key_pair( + private_key: Ed25519PrivateKey | Ed448PrivateKey, +) -> tuple[str, str]: + """Serialize an EdDSA key pair as PEM strings.""" + private_pem = private_key.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.PKCS8, + encryption_algorithm=serialization.NoEncryption(), + ).decode() + public_pem = ( + private_key.public_key() + .public_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PublicFormat.SubjectPublicKeyInfo, + ) + .decode() + ) + return private_pem, public_pem + + +def create_okp_token( + private_key: str, + algorithm: str, + *, + kid: str | None = None, +) -> str: + """Create a JWT signed by an OKP key.""" + header = {"alg": algorithm} + if kid is not None: + header["kid"] = kid + return jwt.encode( + header, + { + "sub": "test-user", + "iss": "https://test.example.com", + "aud": "https://api.example.com", + "exp": int(time.time()) + 3600, + }, + jose_jwk.import_key(private_key, "OKP"), + algorithms=[algorithm], + ) + + @pytest.fixture(scope="module") def symmetric_key_helper() -> SymmetricKeyHelper: """Generate a symmetric key helper for testing.""" @@ -485,6 +531,58 @@ class TestSymmetricKeyJWT: assert access_token is None +class TestEdDSAJWT: + """Tests for JWT verification using Edwards-curve keys.""" + + @pytest.mark.parametrize("algorithm", ["Ed25519", "Ed448"]) + async def test_static_public_key(self, algorithm: str): + """Fully specified EdDSA algorithms verify with a static public key.""" + if algorithm == "Ed25519": + private_key = Ed25519PrivateKey.generate() + else: + private_key = Ed448PrivateKey.generate() + private_pem, public_pem = create_okp_key_pair(private_key) + verifier = JWTVerifier( + public_key=public_pem, + issuer="https://test.example.com", + audience="https://api.example.com", + algorithm=algorithm, + ) + + access_token = await verifier.load_access_token( + create_okp_token(private_pem, algorithm) + ) + + assert access_token is not None + assert access_token.client_id == "test-user" + + @pytest.mark.filterwarnings( + "ignore:EdDSA is deprecated via RFC 9864:joserfc.errors.SecurityWarning" + ) + async def test_legacy_eddsa_jwks( + self, + httpx_mock: HTTPXMock, + ): + """Legacy EdDSA tokens verify against an Ed25519 JWKS entry.""" + private_pem, public_pem = create_okp_key_pair(Ed25519PrivateKey.generate()) + public_jwk = jose_jwk.import_key(public_pem, "OKP").as_dict() + public_jwk.update(kid="ed25519-key", alg="EdDSA", use="sig") + httpx_mock.add_response(json={"keys": [public_jwk]}) + verifier = JWTVerifier( + jwks_uri="https://test.example.com/.well-known/jwks.json", + issuer="https://test.example.com", + audience="https://api.example.com", + algorithm="EdDSA", + ) + + access_token = await verifier.load_access_token( + create_okp_token(private_pem, "EdDSA", kid="ed25519-key") + ) + + assert access_token is not None + assert access_token.client_id == "test-user" + + def _create_token_without_sub( rsa_key_pair: RSAKeyPair, *, @@ -662,7 +760,7 @@ class TestBearerTokenJWKS: assert access_token.claims.get("iss") == issuer assert access_token.claims.get("aud") == audience - async def test_jwks_skips_unsupported_key_types( + async def test_jwks_skips_unusable_keys( self, rsa_key_pair: RSAKeyPair, jwks_provider: JWTVerifier, @@ -670,28 +768,19 @@ class TestBearerTokenJWKS: httpx_mock: HTTPXMock, mock_dns, ): - """An unsupported key type in the JWKS (e.g. OKP/Ed25519) must be - skipped, not poison the whole key set - #4515. - - Some authorization servers (e.g. Rauthy, Ory Hydra) publish an - Ed25519 key alongside RSA keys; tokens signed by the RSA keys must - still verify. - """ - okp_key = cast( + """An unusable key must not poison the whole key set - #4515.""" + malformed_key = cast( "JWKData", { - "kty": "OKP", - "crv": "Ed25519", - "kid": "ed25519-key", - "alg": "EdDSA", + "kty": "RSA", + "kid": "malformed-key", "use": "sig", - "x": "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo", }, ) mock_jwks_data["keys"][0]["kid"] = "test-key-1" - # Unsupported key FIRST, so an unguarded conversion loop would + # Malformed key FIRST, so an unguarded conversion loop would # abort before reaching the RSA key the token needs - mock_jwks_data["keys"].insert(0, okp_key) + mock_jwks_data["keys"].insert(0, malformed_key) httpx_mock.add_response(json=mock_jwks_data) token = rsa_key_pair.create_token( @@ -705,37 +794,60 @@ class TestBearerTokenJWKS: assert access_token is not None assert access_token.client_id == "test-user" - async def test_jwks_with_only_unsupported_keys_rejects_cleanly( + async def test_jwks_ignores_other_algorithm_key_types_without_kid( + self, + rsa_key_pair: RSAKeyPair, + jwks_provider: JWTVerifier, + mock_jwks_data: JWKSData, + httpx_mock: HTTPXMock, + mock_dns, + ): + """Unrelated key types do not make a no-kid lookup ambiguous.""" + _, public_pem = create_okp_key_pair(Ed25519PrivateKey.generate()) + okp_key = jose_jwk.import_key(public_pem, "OKP").as_dict() + okp_key.update(kid="ed25519-key", alg="Ed25519", use="sig") + mock_jwks_data["keys"].append(cast("JWKData", okp_key)) + httpx_mock.add_response(json=mock_jwks_data) + + token = rsa_key_pair.create_token( + subject="test-user", + issuer="https://test.example.com", + audience="https://api.example.com", + ) + + access_token = await jwks_provider.load_access_token(token) + + assert access_token is not None + assert access_token.client_id == "test-user" + + async def test_jwks_with_only_unusable_keys_rejects_cleanly( self, rsa_key_pair: RSAKeyPair, jwks_provider: JWTVerifier, httpx_mock: HTTPXMock, mock_dns, ): - """If every key in the JWKS is unsupported, verification fails + """If every key in the JWKS is unusable, verification fails cleanly (returns None) rather than crashing - #4515.""" - okp_only = { + unusable_only = { "keys": [ cast( "JWKData", { - "kty": "OKP", - "crv": "Ed25519", - "kid": "ed25519-key", - "alg": "EdDSA", + "kty": "RSA", + "kid": "malformed-key", "use": "sig", - "x": "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo", }, ) ] } - httpx_mock.add_response(json=okp_only) + httpx_mock.add_response(json=unusable_only) token = rsa_key_pair.create_token( subject="test-user", issuer="https://test.example.com", audience="https://api.example.com", - kid="ed25519-key", + kid="malformed-key", ) access_token = await jwks_provider.load_access_token(token) diff --git a/uv.lock b/uv.lock index 727bf1183..58fe0e134 100644 --- a/uv.lock +++ b/uv.lock @@ -1046,7 +1046,7 @@ requires-dist = [ { name = "httpx2", marker = "extra == 'client'", specifier = ">=2.5.0" }, { name = "httpx2", marker = "extra == 'mcp'", specifier = ">=2.5.0" }, { name = "httpx2", marker = "extra == 'server'", specifier = ">=2.5.0" }, - { name = "joserfc", marker = "extra == 'server'", specifier = ">=1.1.0" }, + { name = "joserfc", marker = "extra == 'server'", specifier = ">=1.5.0" }, { name = "jsonref", marker = "extra == 'gemini'", specifier = ">=1.1.0" }, { name = "jsonref", marker = "extra == 'server'", specifier = ">=1.1.0" }, { name = "jsonschema-path", marker = "extra == 'server'", specifier = ">=0.3.4" }, From 9feb1f378b06908d3fbd57b8ac7ebc486cb87088 Mon Sep 17 00:00:00 2001 From: Jake Kaplan <40362401+jakekaplan@users.noreply.github.com> Date: Thu, 6 Aug 2026 19:09:05 -0400 Subject: [PATCH 47/53] Forward proxy server metadata across protocol eras (#4776) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Forward proxy negotiation metadata 🤖 Generated with OpenAI Codex * Limit forwarded proxy metadata 🤖 Generated with OpenAI Codex * Tighten negotiation metadata forwarding 🤖 Generated with OpenAI Codex * Tighten proxy metadata docs 🤖 Generated with OpenAI Codex * Keep proxy metadata middleware with provider 🤖 Generated with OpenAI Codex * Simplify proxy negotiation middleware API 🤖 Generated with OpenAI Codex * Name proxy metadata middleware directly 🤖 Generated with OpenAI Codex * Preserve discovery middleware contracts 🤖 Generated with OpenAI Codex * Clarify proxy metadata ownership 🤖 Generated with OpenAI Codex * Align proxy metadata wording 🤖 Generated with OpenAI Codex * Call forwarded values server metadata 🤖 Generated with OpenAI Codex * Harden proxy metadata reads 🤖 Generated with OpenAI Codex * Expose configured discovery result 🤖 Generated with OpenAI Codex * Preserve proxy discovery compatibility 🤖 Generated with OpenAI Codex * Preserve deprecated initialization middleware 🤖 Generated with OpenAI Codex * Harden proxy metadata boundaries 🤖 Generated with OpenAI Codex * Restore deprecated middleware location 🤖 Generated with OpenAI Codex * Simplify proxy metadata client lifecycle 🤖 Generated with OpenAI Codex * Clarify proxy metadata lifecycle 🤖 Generated with OpenAI Codex * Preserve proxy factory errors 🤖 Generated with OpenAI Codex * Detach forwarded proxy metadata 🤖 Generated with OpenAI Codex * Simplify proxy metadata implementation 🤖 Generated with OpenAI Codex * Distinguish proxy metadata failures 🤖 Generated with OpenAI Codex * Narrow proxy metadata validation fallback 🤖 Generated with OpenAI Codex * Retrigger CI 🤖 Generated with OpenAI Codex --- docs/servers/middleware.mdx | 16 + docs/servers/providers/proxy.mdx | 28 +- fastmcp_slim/fastmcp/client/client.py | 5 + fastmcp_slim/fastmcp/server/low_level.py | 67 +- .../fastmcp/server/middleware/middleware.py | 9 + .../fastmcp/server/providers/proxy.py | 274 ++++++-- tests/client/client/test_mode_negotiation.py | 15 +- .../middleware/test_discovery_middleware.py | 108 +++ .../middleware/test_message_visibility.py | 17 + .../providers/proxy/test_proxy_server.py | 67 +- .../providers/proxy/test_server_metadata.py | 637 ++++++++++++++++++ 11 files changed, 1111 insertions(+), 132 deletions(-) create mode 100644 tests/server/middleware/test_discovery_middleware.py create mode 100644 tests/server/providers/proxy/test_server_metadata.py diff --git a/docs/servers/middleware.mdx b/docs/servers/middleware.mdx index dd841f00d..1a0aa96bb 100644 --- a/docs/servers/middleware.mdx +++ b/docs/servers/middleware.mdx @@ -310,6 +310,22 @@ async def on_initialize(self, context: MiddlewareContext, call_next): Rejection works only **before** `call_next()`. Raising `McpError` afterward logs the error without sending it — the client still receives a successful initialize response. </Warning> +#### on_discover + +Called when a modern client negotiates through `server/discover`. Core discovery responses are returned as `DiscoverResult`; extension-owned result types are returned as dictionaries and should be passed through unless the middleware handles that extension. + +```python +from mcp_types import DiscoverResult + +async def on_discover(self, context, call_next): + result = await call_next(context) + if not isinstance(result, DiscoverResult): + return result + return result.model_copy(update={"instructions": "Custom instructions"}) +``` + +Fields such as `supported_versions`, `capabilities`, and cache policy should only be changed when the server's public behavior also changes. + ### Raw Handler For complete control over all messages, override `__call__` instead of individual hooks: diff --git a/docs/servers/providers/proxy.mdx b/docs/servers/providers/proxy.mdx index df09b1c80..1ff116bbd 100644 --- a/docs/servers/providers/proxy.mdx +++ b/docs/servers/providers/proxy.mdx @@ -60,11 +60,9 @@ To mount a proxy inside another FastMCP server, see [Mounting External Servers]( ## Connection Semantics -FastMCP proxies are lazy bridges. Creating the proxy object and starting the local server do not contact the upstream server. The upstream connection begins when an MCP client sends an `initialize` request to the proxy. +FastMCP proxies are lazy bridges. Creating the proxy object and starting the local server do not contact the upstream server. During client negotiation, the proxy makes a best-effort request for optional server metadata using the backend client's existing lifecycle and negotiation mode; an unavailable backend does not prevent the client from connecting to the proxy. -During initialization, the proxy initializes the upstream server before responding locally. If the upstream server is unavailable, the URL does not point to an MCP endpoint, or upstream authentication cannot complete, the proxy initialization fails. This keeps the local proxy's connection status aligned with the upstream server it represents. - -After initialization, the proxy forwards MCP requests such as `ping`, `tools/list`, `resources/list`, `prompts/list`, tool calls, resource reads, sampling, elicitation, logging, and progress through the upstream client. +Subsequent MCP requests such as `ping`, `tools/list`, `resources/list`, `prompts/list`, tool calls, resource reads, sampling, elicitation, logging, and progress connect to the backend as needed. Component provider failures follow `provider_error_strategy`: the default `"warn"` logs and skips a failed provider, while `"raise"` reports the failure to the client. ## Transport Bridging @@ -388,6 +386,28 @@ Only reuse sessions when you know the backend is stateless (e.g. stateless HTTP) ## Advanced Usage +### Forwarding Server Metadata + +Add `ProxyMetadataMiddleware` when a gateway built with `ProxyProvider` should also expose backend instructions and namespaced `_meta`: + +```python +from fastmcp import FastMCP +from fastmcp.server.providers.proxy import ( + ProxyClient, + ProxyMetadataMiddleware, + ProxyProvider, +) + +backend = ProxyProvider(lambda: ProxyClient("http://backend:8000/mcp", mode="auto")) +gateway = FastMCP( + "Controlled Gateway", + providers=[backend], + middleware=[ProxyMetadataMiddleware(backend)], +) +``` + +By default the gateway keeps its own `serverInfo`; pass `identity="upstream"` to use the backend identity when available. Frontend instructions and `_meta` values win on collisions. The middleware never copies upstream protocol versions, connection metadata, capabilities, cache policy, `resultType`, or unknown top-level fields. If the backend is unavailable, the client can still connect without its optional metadata. + ### FastMCPProxy Class For explicit session control, use `FastMCPProxy` directly: diff --git a/fastmcp_slim/fastmcp/client/client.py b/fastmcp_slim/fastmcp/client/client.py index 55aff6408..ae1dde726 100644 --- a/fastmcp_slim/fastmcp/client/client.py +++ b/fastmcp_slim/fastmcp/client/client.py @@ -657,6 +657,11 @@ class Client( return self._session_state.session + @property + def prior_discover(self) -> mcp_types.DiscoverResult | None: + """The configured result to adopt when `mode` pins a modern version.""" + return self._prior_discover + @property def initialize_result(self) -> mcp_types.InitializeResult | None: """Get the result of the initialization request. diff --git a/fastmcp_slim/fastmcp/server/low_level.py b/fastmcp_slim/fastmcp/server/low_level.py index 851bd74a7..cef3ad689 100644 --- a/fastmcp_slim/fastmcp/server/low_level.py +++ b/fastmcp_slim/fastmcp/server/low_level.py @@ -153,10 +153,11 @@ class FastMCPServerMiddleware: Dispatch shapes: - - ``initialize`` runs the *whole* FastMCP chain here (``on_message`` -> - ``on_request`` -> ``on_initialize``) because there is no interior handler - adapter for it: the SDK builds the ``InitializeResult`` directly, so this is - the only place ``on_initialize`` can observe it or veto with ``MCPError``. + - Negotiation runs the *whole* FastMCP chain here: ``initialize`` dispatches + through ``on_initialize`` and ``server/discover`` through ``on_discover``. + Neither has an interior FastMCP handler adapter, and the SDK serializes both + results before returning through its middleware seam, so this root adapter + restores core results to typed models before FastMCP middleware observes them. - The component methods (``tools/call``, ``tools/list``, ``resources/read``, ...) still run their FastMCP chain *interior*, in the handler adapter, where ``on_call_tool`` receives the typed component result and a tool exception @@ -192,6 +193,8 @@ class FastMCPServerMiddleware: return await call_next(ctx) if ctx.method == "initialize" and ctx.request_id is not None: return await self._run_initialize_mw(fastmcp, ctx, call_next) + if ctx.method == "server/discover" and ctx.request_id is not None: + return await self._run_discover_mw(fastmcp, ctx, call_next) if ctx.request_id is not None and ctx.method in _INTERIOR_METHODS: return await self._dispatch_component(fastmcp, ctx, call_next) return await self._run_outer_mw(fastmcp, ctx, call_next, _raise=None) @@ -318,6 +321,62 @@ class FastMCPServerMiddleware: for var, token in reversed(tokens): var.reset(token) + async def _run_discover_mw( + self, + fastmcp: FastMCP, + ctx: ServerRequestContext, + call_next: CallNext, + ) -> HandlerResult: + """Run discovery through the typed FastMCP middleware hook.""" + from fastmcp.server.context import Context + from fastmcp.server.middleware.middleware import MiddlewareContext + + try: + discover_message = mcp_types.DiscoverRequest.model_validate( + {"method": "server/discover", "params": ctx.params}, by_name=False + ) + except ValidationError as exc: + return await self._run_outer_mw(fastmcp, ctx, call_next, _raise=exc) + + async def call_original_handler( + _mw_ctx: MiddlewareContext, + ) -> mcp_types.DiscoverResult | dict[str, Any]: + message = _mw_ctx.message + params = ( + message.params.model_dump(by_alias=True, mode="json", exclude_none=True) + if message.params is not None + else None + ) + raw = await call_next(replace(ctx, params=params)) + if isinstance(raw, mcp_types.DiscoverResult): + return raw + if isinstance(raw, Mapping): + result = dict(raw) + result_type = result.get("resultType") + if ( + isinstance(result_type, str) + and result_type not in mcp_types.CORE_RESULT_TYPES + ): + return result + return mcp_types.DiscoverResult.model_validate(result) + raise TypeError( + "server/discover handler returned " + f"{type(raw).__name__}; expected DiscoverResult or mapping" + ) + + async with Context(fastmcp=fastmcp, session=ctx.session) as fastmcp_ctx: + mw_context = MiddlewareContext( + message=discover_message, + source="client", + type="request", + method="server/discover", + fastmcp_context=fastmcp_ctx, + ) + return await fastmcp._run_middleware( + mw_context, + cast("FastMCPCallNext[Any, Any]", call_original_handler), + ) + async def _run_initialize_mw( self, fastmcp: FastMCP, diff --git a/fastmcp_slim/fastmcp/server/middleware/middleware.py b/fastmcp_slim/fastmcp/server/middleware/middleware.py index 2a112aa5f..87a1b9914 100644 --- a/fastmcp_slim/fastmcp/server/middleware/middleware.py +++ b/fastmcp_slim/fastmcp/server/middleware/middleware.py @@ -170,6 +170,8 @@ class Middleware: match context.method: case "initialize": handler = make_handler_wrapper(self.on_initialize, handler) + case "server/discover": + handler = make_handler_wrapper(self.on_discover, handler) case "tools/call": handler = make_handler_wrapper(self.on_call_tool, handler) case "resources/read": @@ -227,6 +229,13 @@ class Middleware: ) -> mt.InitializeResult | None: return await call_next(context) + async def on_discover( + self, + context: MiddlewareContext[mt.DiscoverRequest], + call_next: CallNext[mt.DiscoverRequest, mt.DiscoverResult | dict[str, Any]], + ) -> mt.DiscoverResult | dict[str, Any]: + return await call_next(context) + async def on_call_tool( self, context: MiddlewareContext[mt.CallToolRequestParams], diff --git a/fastmcp_slim/fastmcp/server/providers/proxy.py b/fastmcp_slim/fastmcp/server/providers/proxy.py index a304eacc3..931c49594 100644 --- a/fastmcp_slim/fastmcp/server/providers/proxy.py +++ b/fastmcp_slim/fastmcp/server/providers/proxy.py @@ -10,9 +10,11 @@ from __future__ import annotations import base64 import inspect import time +import warnings from collections.abc import Awaitable, Callable, Sequence -from dataclasses import replace -from typing import TYPE_CHECKING, Any, cast +from copy import deepcopy +from dataclasses import dataclass, replace +from typing import TYPE_CHECKING, Any, Literal, cast import anyio import httpx2 @@ -29,8 +31,10 @@ from mcp_types import ( TextResourceContents, ) from mcp_types.version import MODERN_PROTOCOL_VERSIONS +from pydantic import ValidationError from pydantic.networks import AnyUrl +from fastmcp._warnings import FastMCPDeprecationWarning from fastmcp.client.client import Client, SDKServer, _connection_failure from fastmcp.client.elicitation import ElicitResult, create_elicitation_callback from fastmcp.client.logging import LogMessage, create_log_callback @@ -72,6 +76,7 @@ logger = get_logger(__name__) # Type alias for client factory functions ClientFactoryT = Callable[[], Client] | Callable[[], Awaitable[Client]] +ProxyIdentity = Literal["proxy", "upstream"] class _ForwardingClientSession(ClientSession): @@ -105,14 +110,26 @@ PROXY_TRANSPORT_OPTIONS = TransportOptions( #: anyio stream error directly. Every proxy entry point that opens a backend #: connection normalizes these into an ``MCPError`` so callers see a protocol #: error instead of a raw transport exception. -_PROXY_TRANSPORT_ERRORS: tuple[type[Exception], ...] = ( - RuntimeError, +_PROXY_TRANSPORT_CAUSES: tuple[type[Exception], ...] = ( TimeoutError, httpx2.HTTPError, anyio.ClosedResourceError, anyio.EndOfStream, anyio.BrokenResourceError, ) +_PROXY_TRANSPORT_ERRORS: tuple[type[Exception], ...] = ( + RuntimeError, + *_PROXY_TRANSPORT_CAUSES, +) + + +def _has_transport_cause(error: RuntimeError) -> bool: + cause = error.__cause__ + while cause is not None: + if isinstance(cause, _PROXY_TRANSPORT_CAUSES): + return True + cause = cause.__cause__ + return False def _proxy_upstream_error(error: Exception) -> MCPError: @@ -161,6 +178,15 @@ def _forwardable_request_meta(ctx: Context | None) -> dict[str, Any] | None: return forwarded or None +def _forwardable_server_meta(meta: dict[str, Any] | None) -> dict[str, Any]: + """Backend result metadata that may cross onto the frontend connection.""" + return { + key: value + for key, value in (meta or {}).items() + if key not in _CONNECTION_META_KEYS and key != mcp_types.SERVER_INFO_META_KEY + } + + def _session_request_meta( meta: dict[str, Any] | None, ) -> mcp_types.RequestParamsMeta | None: @@ -229,7 +255,16 @@ def _stash_proxy_request_context(client: Client, ctx: Context) -> None: class ProxyInitializeMiddleware(Middleware): + """Deprecated middleware for forwarding instructions during initialization.""" + def __init__(self, proxy: FastMCPProxy) -> None: + warnings.warn( + "`ProxyInitializeMiddleware` is deprecated and will be removed in a " + "future release. `FastMCPProxy` now installs " + "`ProxyMetadataMiddleware` automatically.", + FastMCPDeprecationWarning, + stacklevel=2, + ) self.proxy = proxy async def on_initialize( @@ -1085,6 +1120,161 @@ class ProxyProvider(Provider): # because client cleanup is handled per-request +@dataclass(frozen=True) +class _UpstreamServerMetadata: + instructions: str | None + server_info: mcp_types.Implementation | None + meta: dict[str, Any] + + @classmethod + def from_result( + cls, + result: mcp_types.InitializeResult | mcp_types.DiscoverResult, + server_info: mcp_types.Implementation | None, + ) -> _UpstreamServerMetadata: + """Detach forwarded values from the backend session's adopted result.""" + return cls( + instructions=result.instructions, + server_info=( + server_info.model_copy(deep=True) if server_info is not None else None + ), + meta=deepcopy(result.meta or {}), + ) + + @classmethod + def from_client(cls, client: Client) -> _UpstreamServerMetadata | None: + result = client.session.initialize_result or client.session.discover_result + if result is None: + return None + return cls.from_result(result, client.session.server_info) + + @classmethod + def from_discover(cls, result: mcp_types.DiscoverResult) -> _UpstreamServerMetadata: + raw_server_info = (result.meta or {}).get(mcp_types.SERVER_INFO_META_KEY) + try: + server_info = ( + mcp_types.Implementation.model_validate(raw_server_info) + if raw_server_info is not None + else None + ) + except ValidationError: + server_info = None + return cls.from_result(result, server_info) + + +class ProxyMetadataMiddleware(Middleware): + """Forward optional server metadata from a ``ProxyProvider`` backend. + + Instructions and namespaced metadata are forwarded with frontend values + taking precedence. Protocol versions, capabilities, cache policy, and result + type are never copied from the backend. ``identity`` controls whether server + identity remains the gateway's or uses the backend's when available. + """ + + def __init__( + self, + provider: ProxyProvider, + *, + identity: ProxyIdentity = "proxy", + ) -> None: + if identity not in ("proxy", "upstream"): + raise ValueError("identity must be 'proxy' or 'upstream'") + self.provider = provider + self.identity = identity + + async def _read_connected(self, client: Client) -> _UpstreamServerMetadata | None: + """Read metadata without changing the client's adopted negotiation state.""" + if client.mode in MODERN_PROTOCOL_VERSIONS and client.prior_discover is None: + # An exact pin adopts a synthetic result without probing. Read the + # real result directly, but do not adopt it into this borrowed session. + raw = await client.session.send_discover(client.mode) + result_type = raw.get("resultType") + if ( + isinstance(result_type, str) + and result_type not in mcp_types.CORE_RESULT_TYPES + ): + return None + try: + result = mcp_types.DiscoverResult.model_validate(raw) + except ValidationError as error: + logger.debug("Could not read upstream server metadata: %r", error) + return None + return _UpstreamServerMetadata.from_discover(result) + return _UpstreamServerMetadata.from_client(client) + + async def _read_upstream( + self, client: Client, context: Context | None + ) -> _UpstreamServerMetadata | None: + if context is not None: + _stash_proxy_request_context(client, context) + + try: + if client.is_connected(): + return await self._read_connected(client) + async with client: + return await self._read_connected(client) + except (MCPError, *_PROXY_TRANSPORT_ERRORS) as error: + if isinstance(error, RuntimeError) and not _has_transport_cause(error): + raise + logger.debug("Could not read upstream server metadata: %r", error) + return None + + def _updates( + self, + result: mcp_types.InitializeResult | mcp_types.DiscoverResult, + upstream: _UpstreamServerMetadata, + ) -> dict[str, Any]: + meta = _forwardable_server_meta(upstream.meta) + meta.update(result.meta or {}) + + updates: dict[str, Any] = {"meta": meta or None} + if result.instructions is None and upstream.instructions is not None: + updates["instructions"] = upstream.instructions + if self.identity == "upstream" and upstream.server_info is not None: + if isinstance(result, mcp_types.InitializeResult): + updates["server_info"] = upstream.server_info + else: + meta[mcp_types.SERVER_INFO_META_KEY] = upstream.server_info.model_dump( + by_alias=True, mode="json", exclude_none=True + ) + updates["meta"] = meta + return updates + + async def on_initialize( + self, + context: MiddlewareContext[mcp_types.InitializeRequest], + call_next: CallNext[ + mcp_types.InitializeRequest, mcp_types.InitializeResult | None + ], + ) -> mcp_types.InitializeResult | None: + # Factory errors must occur before the legacy response is committed. + client = await self.provider._get_client() + result = await call_next(context) + if result is None: + return None + upstream = await self._read_upstream(client, context.fastmcp_context) + if upstream is None: + return result + return result.model_copy(update=self._updates(result, upstream)) + + async def on_discover( + self, + context: MiddlewareContext[mcp_types.DiscoverRequest], + call_next: CallNext[ + mcp_types.DiscoverRequest, + mcp_types.DiscoverResult | dict[str, Any], + ], + ) -> mcp_types.DiscoverResult | dict[str, Any]: + result = await call_next(context) + if not isinstance(result, mcp_types.DiscoverResult): + return result + client = await self.provider._get_client() + upstream = await self._read_upstream(client, context.fastmcp_context) + if upstream is None: + return result + return result.model_copy(update=self._updates(result, upstream)) + + # ----------------------------------------------------------------------------- # Factory Functions # ----------------------------------------------------------------------------- @@ -1266,6 +1456,7 @@ class FastMCPProxy(FastMCP): *, client_factory: ClientFactoryT, provider_error_strategy: ProviderErrorStrategy = "warn", + identity: ProxyIdentity = "proxy", **kwargs, ): """Initialize the proxy server. @@ -1280,16 +1471,18 @@ class FastMCPProxy(FastMCP): provider_error_strategy: How provider errors should affect aggregate operations. Defaults to ``"warn"`` for compatibility; use ``"raise"`` when the proxy should surface upstream failures. + identity: Whether clients see the proxy's server identity or the + upstream server's when available. Defaults to ``"proxy"`` + for compatibility. **kwargs: Additional settings for the FastMCP server. """ super().__init__(**kwargs) self.provider_error_strategy = provider_error_strategy self.client_factory = client_factory - provider: Provider = ProxyProvider(client_factory) + provider = ProxyProvider(client_factory) self.add_provider(provider) - self.middleware.append(ProxyInitializeMiddleware(self)) + self.middleware.append(ProxyMetadataMiddleware(provider, identity=identity)) self._setup_proxy_ping_handler() - self._setup_proxy_discover_handler() async def _get_client(self) -> Client: client = self.client_factory() @@ -1311,73 +1504,6 @@ class FastMCPProxy(FastMCP): "ping", mcp_types.RequestParams, ping_remote ) - def _setup_proxy_discover_handler(self) -> None: - """Forward the backend's instructions on the modern (`server/discover`) path. - - `ProxyInitializeMiddleware` forwards upstream instructions by patching - the `InitializeResult`, but `on_initialize` only fires for the legacy - handshake. A modern client negotiates via `server/discover`, whose - default SDK handler reads `self.instructions` off the low-level server - directly, so a proxy would silently drop its upstream's instructions for - every modern client. - - The SDK sanctions replacing this handler wholesale, so we delegate to - its own implementation for the rest of the result (supported versions, - capabilities, server info) and only fill in the instructions we would - otherwise lose. Resolving them here — at request time, from a live - backend session — keeps the proxy's lazy-connect contract intact: the - backend is contacted when a client actually asks, never at construction. - """ - build_default_result = self._mcp_server._handle_discover - - async def discover_remote( - ctx: ServerRequestContext[Any, Any], - params: mcp_types.RequestParams | None, - ) -> mcp_types.DiscoverResult: - result = await build_default_result(ctx, params) - # A proxy with its own instructions keeps them, matching the - # precedence `ProxyInitializeMiddleware` applies on the legacy path. - if result.instructions is not None: - return result - client = await self._get_client() - # `session.instructions` is era-neutral: it reads the backend's - # `DiscoverResult` or `InitializeResult` depending on what the - # backend negotiated, so a modern front can proxy a legacy backend. - if client.is_connected(): - result.instructions = client.session.instructions - return result - # Era mirroring pins a modern backend to an exact version, and a - # pinned version adopts a synthesized `DiscoverResult` instead of - # probing the wire — so the pinned client would report no - # instructions at all. Instructions are metadata with no - # back-channel, so this read does not need the era consistency - # mirroring exists to protect; negotiate with "auto" instead, which - # probes `server/discover` and falls back to the handshake for a - # legacy-only backend. - client.mode = "auto" - try: - async with client: - result.instructions = client.session.instructions - except (MCPError, *_PROXY_TRANSPORT_ERRORS) as error: - # Instructions are optional metadata, so an unreachable backend - # must not fail negotiation itself. Failing here would surface - # as a confusing protocol error: the client's auto-negotiation - # reads any `server/discover` error as "not a modern server" - # and retries with the initialize handshake, which this - # modern-serving proxy then rejects — hiding the real cause. - # Answer without upstream instructions instead and let the - # backend failure surface on the first real operation, where - # the proxy reports it as an upstream connection error. - logger.debug( - "Could not read upstream instructions for server/discover: %r", - error, - ) - return result - - self._mcp_server.add_request_handler( - "server/discover", mcp_types.RequestParams, discover_remote - ) - # ----------------------------------------------------------------------------- # ProxyClient and Related diff --git a/tests/client/client/test_mode_negotiation.py b/tests/client/client/test_mode_negotiation.py index 1d400a727..97c1fb3a8 100644 --- a/tests/client/client/test_mode_negotiation.py +++ b/tests/client/client/test_mode_negotiation.py @@ -22,7 +22,7 @@ from typing import Any import pytest from mcp import ClientSession from mcp.shared.exceptions import MCPError -from mcp_types import METHOD_NOT_FOUND +from mcp_types import METHOD_NOT_FOUND, DiscoverResult, ServerCapabilities from mcp_types.version import LATEST_HANDSHAKE_VERSION, LATEST_MODERN_VERSION from typing_extensions import Unpack @@ -242,6 +242,19 @@ class TestNonConformantModernPeer: class TestPinnedMode: + def test_prior_discover_is_exposed(self, fastmcp_server): + prior = DiscoverResult( + supported_versions=[LATEST_MODERN_VERSION], + capabilities=ServerCapabilities(), + ) + client = Client( + fastmcp_server, + mode=LATEST_MODERN_VERSION, + prior_discover=prior, + ) + + assert client.prior_discover is prior + async def test_pinned_modern_adopts_without_probe(self, fastmcp_server): """Pinning the modern version adopts it directly; a synthesized DiscoverResult carries no identity, so server_info is absent.""" diff --git a/tests/server/middleware/test_discovery_middleware.py b/tests/server/middleware/test_discovery_middleware.py new file mode 100644 index 000000000..6b8bd8f09 --- /dev/null +++ b/tests/server/middleware/test_discovery_middleware.py @@ -0,0 +1,108 @@ +"""Tests for typed middleware support during modern discovery.""" + +from typing import Any + +import mcp_types +from mcp_types.version import LATEST_MODERN_VERSION + +from fastmcp import Client, FastMCP +from fastmcp.server.middleware import CallNext, Middleware, MiddlewareContext + + +async def test_on_discover_receives_and_transforms_typed_result(): + class DiscoveryMiddleware(Middleware): + def __init__(self) -> None: + self.request: mcp_types.DiscoverRequest | None = None + self.result: mcp_types.DiscoverResult | None = None + + async def on_discover( + self, + context: MiddlewareContext[mcp_types.DiscoverRequest], + call_next: CallNext[ + mcp_types.DiscoverRequest, + mcp_types.DiscoverResult | dict[str, Any], + ], + ) -> mcp_types.DiscoverResult | dict[str, Any]: + self.request = context.message + result = await call_next(context) + assert isinstance(result, mcp_types.DiscoverResult) + self.result = result + return result.model_copy(update={"instructions": "discovered"}) + + middleware = DiscoveryMiddleware() + server = FastMCP("typed-discovery", middleware=[middleware]) + + async with Client(server, mode="auto") as client: + assert client.instructions == "discovered" + + assert isinstance(middleware.request, mcp_types.DiscoverRequest) + assert isinstance(middleware.result, mcp_types.DiscoverResult) + + +async def test_on_discover_forwards_modified_params(): + modified = False + server = FastMCP("modified-discovery") + default_handler = server._mcp_server._handle_discover + + async def capture_params(ctx, params): + nonlocal modified + assert params is not None + assert params.meta is not None + modified = params.meta["com.example/modified"] is True + return await default_handler(ctx, params) + + server._mcp_server.add_request_handler( + "server/discover", mcp_types.RequestParams, capture_params + ) + + class ModifyParams(Middleware): + async def on_discover(self, context, call_next): + assert context.message.params is not None + assert context.message.params.meta is not None + context.message.params = mcp_types.RequestParams( + meta={ + **context.message.params.meta, + "com.example/modified": True, + } + ) + return await call_next(context) + + server.add_middleware(ModifyParams()) + + async with Client(server, mode="auto"): + pass + + assert modified + + +async def test_on_discover_preserves_extension_owned_result(): + extension_result = { + "resultType": "com.example/custom", + "payload": {"enabled": True}, + } + + async def custom_discover(_ctx, _params): + return extension_result + + class ObserveExtension(Middleware): + def __init__(self) -> None: + self.result: mcp_types.DiscoverResult | dict[str, Any] | None = None + + async def on_discover(self, context, call_next): + self.result = await call_next(context) + return self.result + + middleware = ObserveExtension() + server = FastMCP("extension-discovery", middleware=[middleware]) + server._mcp_server.add_request_handler( + "server/discover", mcp_types.RequestParams, custom_discover + ) + + async with Client(server, mode=LATEST_MODERN_VERSION) as client: + result = await client.session.send_discover(LATEST_MODERN_VERSION) + + assert isinstance(result, dict) + assert result["resultType"] == "com.example/custom" + assert result["payload"] == {"enabled": True} + assert isinstance(middleware.result, dict) + assert middleware.result["payload"] == {"enabled": True} diff --git a/tests/server/middleware/test_message_visibility.py b/tests/server/middleware/test_message_visibility.py index 8145c2b06..5c2465637 100644 --- a/tests/server/middleware/test_message_visibility.py +++ b/tests/server/middleware/test_message_visibility.py @@ -159,6 +159,23 @@ class TestUnroutableAndMalformed: assert ("on_message", "tools/call") in recorder.records assert ("on_call_tool", "tools/call") not in recorder.records + async def test_malformed_discover_params_observed_by_generic_hooks(self): + server = _adder() + recorder = HookRecorder() + server.add_middleware(recorder) + + async with Client(server) as client: + recorder.records.clear() + with pytest.raises(MCPError): + await _raw_request( + client, + "server/discover", + {"_meta": {"progressToken": []}}, + ) + + assert ("on_message", "server/discover") in recorder.records + assert ("on_request", "server/discover") in recorder.records + class TestSingleFire: async def test_each_hook_fires_once_per_component_call(self): diff --git a/tests/server/providers/proxy/test_proxy_server.py b/tests/server/providers/proxy/test_proxy_server.py index fe489d56f..997c65700 100644 --- a/tests/server/providers/proxy/test_proxy_server.py +++ b/tests/server/providers/proxy/test_proxy_server.py @@ -204,13 +204,7 @@ async def test_create_proxy_with_transport(fastmcp_server): async def test_proxy_forwards_upstream_instructions(): - """A proxy should surface the upstream server's instructions in the handshake. - - `FastMCPProxy` registers a `server/discover` handler that forwards the - upstream's instructions, mirroring what `ProxyInitializeMiddleware.on_initialize` - already does for the legacy handshake, so `client.session.instructions` - (era-neutral) resolves the same way on both protocol eras. - """ + """The metadata middleware forwards upstream instructions.""" upstream = FastMCP(name="upstream", instructions="USE_THIS_MARKER_123") proxy = create_proxy(upstream, name="proxy") @@ -274,35 +268,25 @@ async def test_proxy_ping_surfaces_wrong_remote_path(): async with run_server_async(remote, transport="http") as url: proxy = create_proxy(StreamableHttpTransport(url.removesuffix("/mcp"))) - # This asserts the error surfaces from merely *connecting* to the proxy, - # with no operation performed. That only happens on the legacy handshake: - # `ProxyInitializeMiddleware.on_initialize` eagerly probes the backend - # during the front's own `initialize` call. A modern front negotiates - # `server/discover` instead, which never runs that middleware hook, so - # connecting succeeds regardless of backend health and the failure would - # only surface on first real use. Pinned because the subject here is - # that eager, handshake-time probe. - # - # SDK v2 surfaces a wrong remote path as an HTTP "Not Found" rather than - # the v1 "Session terminated" message. - with pytest.raises(MCPError, match="Not Found"): - async with Client(proxy, mode="legacy"): - pass + # Optional metadata lookup is best-effort, so the client can connect. The + # first real proxied operation reports the bad backend path instead. + async with Client(proxy, mode="legacy") as client: + with pytest.raises(MCPError, match="Not Found"): + await client.ping() -async def test_proxy_initialize_forwards_remote_connection_error(): +async def test_proxy_initialize_defers_remote_connection_error(): port = find_available_port() proxy = create_proxy( StreamableHttpTransport(f"http://127.0.0.1:{port}/mcp"), provider_error_strategy="raise", ) - # Same reasoning as test_proxy_ping_surfaces_wrong_remote_path above: the - # error surfaces from connecting alone only via the legacy handshake's - # eager backend probe in `ProxyInitializeMiddleware.on_initialize`. - with pytest.raises(MCPError, match="Client failed to connect"): - async with Client(proxy, mode="legacy"): - pass + # The client can connect without optional backend metadata; the first + # component operation reports the unavailable backend. + async with Client(proxy, mode="legacy") as client: + with pytest.raises(MCPError, match="Client failed to connect"): + await client.list_tools() async def test_proxy_list_tools_surfaces_remote_connection_error(): @@ -324,13 +308,10 @@ async def test_proxy_list_tools_surfaces_remote_connection_error(): async def test_proxy_list_tools_client_surfaces_remote_connection_error(): - """With a modern front, connecting succeeds (no eager backend probe — see - test_proxy_ping_surfaces_wrong_remote_path) and the failure only surfaces - once `list_tools()` actually hits the dead backend. `ProxyProvider._list_tools` - now normalizes the raw `httpx2.ConnectError` from the failed backend connect - into the `MCPError("Client failed to connect...")` this test expects, the - same way `ProxyInitializeMiddleware.on_initialize` and `ProxyTool.run` - already did. + """Connecting succeeds and the first component operation reports the backend. + + `ProxyProvider._list_tools` normalizes the raw transport failure into the + `MCPError("Client failed to connect...")` this test expects. """ port = find_available_port() proxy = create_proxy( @@ -1459,13 +1440,7 @@ class TestProxyForwardingAppliesToEveryBackendClient: class TestProxyModernEraInstructions: - """Upstream instructions must reach a client on the modern era too. - - `ProxyInitializeMiddleware.on_initialize` only fires for the legacy - handshake. A `mode="auto"` client negotiates via `server/discover`, which - the SDK builds from the low-level server's own `instructions`, so without a - discover-side hook the proxy drops its upstream's instructions entirely. - """ + """Upstream instructions must reach a client on the modern era too.""" async def test_proxy_forwards_upstream_instructions_on_modern_era(self): upstream = FastMCP(name="upstream", instructions="USE_THIS_MARKER_123") @@ -1491,13 +1466,7 @@ class TestProxyModernEraInstructions: class TestProxyProviderTransportErrors: - """A dead backend must surface as an MCPError, not a raw transport error. - - `ProxyTool.run` and `ProxyInitializeMiddleware.on_initialize` normalize - connection failures into `MCPError`; the provider's list methods caught - only `MCPError`, so an `httpx2.ConnectError` (or the `RuntimeError` the - client wraps a failed connect in) escaped unwrapped to the caller. - """ + """A dead backend must surface as an MCPError, not a raw transport error.""" @pytest.fixture def unreachable_provider(self) -> ProxyProvider: diff --git a/tests/server/providers/proxy/test_server_metadata.py b/tests/server/providers/proxy/test_server_metadata.py new file mode 100644 index 000000000..802235c95 --- /dev/null +++ b/tests/server/providers/proxy/test_server_metadata.py @@ -0,0 +1,637 @@ +"""Server metadata forwarding across proxy protocol eras.""" + +from itertools import product +from typing import Any, Literal, TypeVar + +import mcp_types +import pytest +from mcp import MCPError +from mcp_types.version import MODERN_PROTOCOL_VERSIONS + +from fastmcp import Client, FastMCP, FastMCPDeprecationWarning +from fastmcp.client.logging import LogMessage +from fastmcp.client.transports import StreamableHttpTransport +from fastmcp.server import create_proxy +from fastmcp.server.middleware import CallNext, Middleware, MiddlewareContext +from fastmcp.server.providers.proxy import ( + FastMCPProxy, + ProxyClient, + ProxyInitializeMiddleware, + ProxyMetadataMiddleware, + ProxyProvider, + StatefulProxyClient, +) +from fastmcp.utilities.http import find_available_port + +ResultT = TypeVar("ResultT", bound=mcp_types.Result) + +UPSTREAM_INFO = mcp_types.Implementation( + name="upstream", + title="Upstream title", + version="1.2.3", + description="Upstream description", + website_url="https://upstream.example.com", + icons=[mcp_types.Icon(src="https://upstream.example.com/icon.png")], +) + + +class UpstreamMetadataMiddleware(Middleware): + """Advertise metadata that differs from the gateway's own claims.""" + + def __init__(self, server_info: mcp_types.Implementation = UPSTREAM_INFO) -> None: + self.server_info = server_info + + def _updates(self, result: mcp_types.Result) -> dict[str, Any]: + meta = { + **(result.meta or {}), + mcp_types.PROTOCOL_VERSION_META_KEY: "upstream-version", + mcp_types.CLIENT_INFO_META_KEY: {"name": "upstream-client"}, + mcp_types.CLIENT_CAPABILITIES_META_KEY: {"upstream": True}, + "com.example/upstream": {"enabled": True}, + "com.example/shared": "upstream", + } + updates: dict[str, Any] = { + "instructions": "upstream instructions", + "meta": meta, + } + if isinstance(result, mcp_types.InitializeResult): + updates.update( + server_info=self.server_info, + capabilities=mcp_types.ServerCapabilities( + experimental={"upstream": {"claimed": True}} + ), + ) + else: + meta[mcp_types.SERVER_INFO_META_KEY] = self.server_info.model_dump( + by_alias=True, mode="json", exclude_none=True + ) + updates.update( + ttl_ms=91_000, + cache_scope="public", + capabilities=mcp_types.ServerCapabilities( + experimental={"upstream": {"claimed": True}} + ), + ) + return updates + + async def on_initialize( + self, + context: MiddlewareContext[mcp_types.InitializeRequest], + call_next: CallNext[ + mcp_types.InitializeRequest, mcp_types.InitializeResult | None + ], + ) -> mcp_types.InitializeResult | None: + result = await call_next(context) + assert result is not None + return result.model_copy(update=self._updates(result)) + + async def on_discover( + self, + context: MiddlewareContext[mcp_types.DiscoverRequest], + call_next: CallNext[ + mcp_types.DiscoverRequest, + mcp_types.DiscoverResult | dict[str, Any], + ], + ) -> mcp_types.DiscoverResult | dict[str, Any]: + result = await call_next(context) + if not isinstance(result, mcp_types.DiscoverResult): + return result + return result.model_copy(update=self._updates(result)) + + +class FrontendMetadataMiddleware(Middleware): + """Set frontend values that must win over the upstream on collision.""" + + def _update(self, result: ResultT) -> ResultT: + return result.model_copy( + update={ + "meta": { + **(result.meta or {}), + "com.example/shared": "frontend", + "com.example/frontend": {"enabled": True}, + }, + } + ) + + async def on_initialize( + self, + context: MiddlewareContext[mcp_types.InitializeRequest], + call_next: CallNext[ + mcp_types.InitializeRequest, mcp_types.InitializeResult | None + ], + ) -> mcp_types.InitializeResult | None: + result = await call_next(context) + assert result is not None + return self._update(result) + + async def on_discover( + self, + context: MiddlewareContext[mcp_types.DiscoverRequest], + call_next: CallNext[ + mcp_types.DiscoverRequest, + mcp_types.DiscoverResult | dict[str, Any], + ], + ) -> mcp_types.DiscoverResult | dict[str, Any]: + result = await call_next(context) + if not isinstance(result, mcp_types.DiscoverResult): + return result + return self._update(result) + + +def make_upstream() -> FastMCP: + return FastMCP("unmodified-upstream", middleware=[UpstreamMetadataMiddleware()]) + + +def make_gateway( + upstream: FastMCP, + *, + backend_mode: str, + identity: Literal["proxy", "upstream"] = "proxy", + instructions: str | None = None, + frontend_metadata: bool = False, +) -> FastMCP: + provider = ProxyProvider(lambda: ProxyClient(upstream, mode=backend_mode)) + metadata = ProxyMetadataMiddleware(provider, identity=identity) + middleware: list[Middleware] = [metadata] + if frontend_metadata: + middleware.append(FrontendMetadataMiddleware()) + gateway = FastMCP( + "gateway", + version="9.8.7", + instructions=instructions, + providers=[provider], + middleware=middleware, + cache_ttl=7, + cache_scope="private", + ) + return gateway + + +@pytest.mark.parametrize( + ("frontend_mode", "backend_mode"), + list(product(("legacy", "auto"), repeat=2)), +) +async def test_forwards_metadata_across_all_protocol_era_combinations( + frontend_mode: str, backend_mode: str +): + gateway = make_gateway(make_upstream(), backend_mode=backend_mode) + + async with Client(gateway, mode=frontend_mode) as client: + result = client.session.initialize_result or client.session.discover_result + assert result is not None + assert client.instructions == "upstream instructions" + assert client.server_info is not None + assert client.server_info.name == "gateway" + assert result.meta is not None + assert result.meta["com.example/upstream"] == {"enabled": True} + for key in ( + mcp_types.PROTOCOL_VERSION_META_KEY, + mcp_types.CLIENT_INFO_META_KEY, + mcp_types.CLIENT_CAPABILITIES_META_KEY, + ): + assert key not in result.meta + stamped_info = result.meta.get(mcp_types.SERVER_INFO_META_KEY) + assert result.capabilities.experimental is None + + if isinstance(result, mcp_types.InitializeResult): + assert result.protocol_version not in MODERN_PROTOCOL_VERSIONS + assert stamped_info is None + else: + assert stamped_info is not None + assert stamped_info["name"] == "gateway" + assert result.supported_versions == list(MODERN_PROTOCOL_VERSIONS) + assert result.ttl_ms == 7_000 + assert result.cache_scope == "private" + assert result.result_type == "complete" + + +@pytest.mark.parametrize("frontend_mode", ["legacy", "auto"]) +@pytest.mark.parametrize("identity", ["proxy", "upstream"]) +async def test_identity_policy_forwards_full_implementation( + frontend_mode: str, identity: Literal["proxy", "upstream"] +): + gateway = make_gateway(make_upstream(), backend_mode="auto", identity=identity) + + async with Client(gateway, mode=frontend_mode) as client: + assert client.server_info is not None + if identity == "proxy": + assert client.server_info.name == "gateway" + assert client.server_info.version == "9.8.7" + else: + assert client.server_info == UPSTREAM_INFO + result = client.session.initialize_result or client.session.discover_result + assert result is not None + if isinstance(result, mcp_types.InitializeResult): + assert mcp_types.SERVER_INFO_META_KEY not in (result.meta or {}) + else: + assert result.meta is not None + assert result.meta[mcp_types.SERVER_INFO_META_KEY]["name"] == "upstream" + + +@pytest.mark.parametrize("frontend_mode", ["legacy", "auto"]) +async def test_frontend_values_take_precedence(frontend_mode: str): + gateway = make_gateway( + make_upstream(), + backend_mode="auto", + instructions="frontend instructions", + frontend_metadata=True, + ) + + async with Client(gateway, mode=frontend_mode) as client: + result = client.session.initialize_result or client.session.discover_result + assert result is not None + assert client.instructions == "frontend instructions" + assert result.meta is not None + assert result.meta["com.example/shared"] == "frontend" + assert result.meta["com.example/frontend"] == {"enabled": True} + assert result.meta["com.example/upstream"] == {"enabled": True} + + +async def test_forwards_backend_logs_while_reading_metadata(): + messages: list[str] = [] + + class LogOnInitialize(Middleware): + async def on_initialize( + self, + context: MiddlewareContext[mcp_types.InitializeRequest], + call_next: CallNext[ + mcp_types.InitializeRequest, mcp_types.InitializeResult | None + ], + ) -> mcp_types.InitializeResult | None: + result = await call_next(context) + assert context.fastmcp_context is not None + await context.fastmcp_context.log("metadata connection") + return result + + async def capture_log(message: LogMessage) -> None: + messages.append(message.data["msg"]) + + upstream = FastMCP("upstream", middleware=[LogOnInitialize()]) + proxy = create_proxy(upstream) + + async with Client(proxy, mode="legacy", log_handler=capture_log): + pass + + assert messages == ["metadata connection"] + + +async def test_pinned_client_uses_prior_discover_metadata(): + prior_info = mcp_types.Implementation(name="prior", version="1.0") + prior = mcp_types.DiscoverResult( + supported_versions=[MODERN_PROTOCOL_VERSIONS[0]], + capabilities=mcp_types.ServerCapabilities(), + instructions="prior instructions", + meta={ + mcp_types.SERVER_INFO_META_KEY: prior_info.model_dump( + by_alias=True, mode="json" + ), + "com.example/prior": True, + }, + ) + provider = ProxyProvider( + lambda: ProxyClient( + make_upstream(), + mode=MODERN_PROTOCOL_VERSIONS[0], + prior_discover=prior, + ) + ) + gateway = FastMCP( + "gateway", + providers=[provider], + middleware=[ProxyMetadataMiddleware(provider, identity="upstream")], + ) + + async with Client(gateway, mode="auto") as client: + result = client.session.discover_result + assert result is not None + assert client.instructions == "prior instructions" + assert client.server_info == prior_info + assert result.meta is not None + assert result.meta["com.example/prior"] is True + + +async def test_connected_pinned_client_probes_without_adopting_metadata(): + version = MODERN_PROTOCOL_VERSIONS[0] + upstream = make_upstream() + async with Client(upstream, mode=version) as backend_client: + assert backend_client.instructions is None + proxy = create_proxy(backend_client, identity="upstream") + + async with Client(proxy, mode="auto") as client: + result = client.session.discover_result + assert result is not None + assert client.instructions == "upstream instructions" + assert client.server_info == UPSTREAM_INFO + assert result.meta is not None + assert result.meta["com.example/upstream"] == {"enabled": True} + + assert backend_client.instructions is None + + +async def test_invalid_upstream_discovery_metadata_is_ignored( + monkeypatch: pytest.MonkeyPatch, +): + version = MODERN_PROTOCOL_VERSIONS[0] + + async def invalid_discover(_version: str) -> dict[str, Any]: + return { + "resultType": "complete", + "supportedVersions": [version], + "capabilities": [], + } + + async with ProxyClient(make_upstream(), mode=version) as backend_client: + monkeypatch.setattr(backend_client.session, "send_discover", invalid_discover) + proxy = create_proxy(backend_client) + + async with Client(proxy, mode="auto") as client: + assert client.server_info is not None + assert client.server_info.name == proxy.name + assert await client.list_tools() == [] + + +async def test_invalid_backend_client_negotiation_is_not_ignored(): + version = MODERN_PROTOCOL_VERSIONS[0] + prior = mcp_types.DiscoverResult( + supported_versions=["2099-01-01"], + capabilities=mcp_types.ServerCapabilities(), + ) + provider = ProxyProvider( + lambda: ProxyClient( + make_upstream(), + mode=version, + prior_discover=prior, + ) + ) + gateway = FastMCP( + "gateway", + providers=[provider], + middleware=[ProxyMetadataMiddleware(provider)], + ) + + with pytest.raises(MCPError): + async with Client(gateway, mode="auto"): + pass + + +async def test_unrelated_client_validation_error_is_not_ignored(): + class InvalidClient(ProxyClient): + async def __aenter__(self) -> ProxyClient: + mcp_types.Implementation.model_validate({}) + return self + + provider = ProxyProvider(lambda: InvalidClient(make_upstream())) + gateway = FastMCP( + "gateway", + providers=[provider], + middleware=[ProxyMetadataMiddleware(provider)], + ) + + with pytest.raises(MCPError): + async with Client(gateway, mode="auto"): + pass + + +@pytest.mark.parametrize("mode", ["legacy", "auto"]) +async def test_forwarded_metadata_does_not_alias_connected_backend(mode: str): + backend_info = mcp_types.Implementation(name="shared-backend", version="1.0") + upstream = FastMCP( + "upstream", + middleware=[UpstreamMetadataMiddleware(backend_info)], + ) + + class MutateForwardedMetadata(Middleware): + def _mutate(self, result: ResultT) -> ResultT: + assert result.meta is not None + nested = result.meta["com.example/upstream"] + assert isinstance(nested, dict) + nested["enabled"] = False + if isinstance(result, mcp_types.InitializeResult): + result.server_info.name = "frontend mutation" + else: + server_info = result.meta[mcp_types.SERVER_INFO_META_KEY] + assert isinstance(server_info, dict) + server_info["name"] = "frontend mutation" + return result + + async def on_initialize(self, context, call_next): + result = await call_next(context) + assert result is not None + return self._mutate(result) + + async def on_discover(self, context, call_next): + result = await call_next(context) + if not isinstance(result, mcp_types.DiscoverResult): + return result + return self._mutate(result) + + async with Client(upstream, mode=mode) as backend_client: + provider = ProxyProvider(lambda: backend_client) + gateway = FastMCP( + "gateway", + providers=[provider], + middleware=[ + MutateForwardedMetadata(), + ProxyMetadataMiddleware(provider, identity="upstream"), + ], + ) + + async with Client(gateway, mode=mode): + pass + + backend_result = ( + backend_client.session.initialize_result + or backend_client.session.discover_result + ) + assert backend_result is not None + assert backend_result.meta is not None + assert backend_result.meta["com.example/upstream"] == {"enabled": True} + assert backend_client.server_info == backend_info + + +async def test_disconnected_pinned_client_is_not_cloned(): + class UnclonableProxyClient(ProxyClient): + def new(self) -> ProxyClient: + raise AssertionError("metadata client must not be cloned") + + version = MODERN_PROTOCOL_VERSIONS[0] + provider = ProxyProvider( + lambda: UnclonableProxyClient(make_upstream(), mode=version) + ) + gateway = FastMCP( + "gateway", + providers=[provider], + middleware=[ProxyMetadataMiddleware(provider, identity="upstream")], + ) + + async with Client(gateway, mode="auto") as client: + assert client.instructions == "upstream instructions" + assert client.server_info == UPSTREAM_INFO + + +async def test_stateful_pinned_metadata_uses_registered_client_lifecycle(): + created: list[StatefulProxyClient] = [] + + class TrackingStatefulProxyClient(StatefulProxyClient): + def new(self) -> StatefulProxyClient: + client = super().new() + created.append(client) + return client + + version = MODERN_PROTOCOL_VERSIONS[0] + stateful_client = TrackingStatefulProxyClient(make_upstream(), mode=version) + proxy = FastMCPProxy( + name="stateful-proxy", + client_factory=stateful_client.new_stateful, + identity="upstream", + ) + + async with Client(proxy, mode="auto") as client: + assert client.instructions == "upstream instructions" + assert client.server_info == UPSTREAM_INFO + + assert len(created) == 1 + assert not created[0].is_connected() + + +@pytest.mark.parametrize("frontend_mode", ["legacy", "auto"]) +@pytest.mark.parametrize("async_factory", [False, True]) +@pytest.mark.parametrize("error_kind", ["runtime", "mcp"]) +async def test_client_factory_errors_are_not_swallowed( + frontend_mode: str, + async_factory: bool, + error_kind: Literal["runtime", "mcp"], +): + def factory_error() -> Exception: + if error_kind == "mcp": + return MCPError( + code=mcp_types.INTERNAL_ERROR, + message="broken client factory", + ) + return RuntimeError("broken client factory") + + def broken_factory() -> Client: + raise factory_error() + + async def broken_async_factory() -> Client: + raise factory_error() + + factory = broken_async_factory if async_factory else broken_factory + provider = ProxyProvider(factory) + gateway = FastMCP( + "gateway", + providers=[provider], + middleware=[ProxyMetadataMiddleware(provider)], + ) + + with pytest.raises(MCPError): + async with Client(gateway, mode=frontend_mode): + pass + + +@pytest.mark.parametrize("frontend_mode", ["legacy", "auto"]) +async def test_unavailable_backend_does_not_block_connection(frontend_mode: str): + port = find_available_port() + provider = ProxyProvider( + lambda: ProxyClient( + StreamableHttpTransport(f"http://127.0.0.1:{port}/mcp"), mode="auto" + ), + cache_ttl=0, + ) + gateway = FastMCP( + "available-gateway", + providers=[provider], + middleware=[ProxyMetadataMiddleware(provider)], + ) + gateway.provider_error_strategy = "raise" + + async with Client(gateway, mode=frontend_mode) as client: + assert client.server_info is not None + assert client.server_info.name == "available-gateway" + with pytest.raises(MCPError, match="Client failed to connect"): + await client.list_tools() + + +async def test_extension_owned_discovery_result_bypasses_metadata_forwarding(): + factory_called = False + + def broken_factory() -> Client: + nonlocal factory_called + factory_called = True + raise RuntimeError("metadata should not be read") + + async def custom_discover(_ctx, _params): + return { + "resultType": "com.example/custom", + "payload": {"enabled": True}, + } + + provider = ProxyProvider(broken_factory) + gateway = FastMCP( + "extension-gateway", + middleware=[ProxyMetadataMiddleware(provider)], + ) + gateway._mcp_server.add_request_handler( + "server/discover", mcp_types.RequestParams, custom_discover + ) + + version = MODERN_PROTOCOL_VERSIONS[0] + async with Client(gateway, mode=version) as client: + result = await client.session.send_discover(version) + + assert isinstance(result, dict) + assert result["payload"] == {"enabled": True} + assert not factory_called + + +def test_gateway_construction_does_not_create_backend_client(): + calls = 0 + + def client_factory() -> ProxyClient: + nonlocal calls + calls += 1 + return ProxyClient(make_upstream()) + + provider = ProxyProvider(client_factory) + FastMCP( + "lazy-gateway", + providers=[provider], + middleware=[ProxyMetadataMiddleware(provider)], + ) + + assert calls == 0 + + +async def test_proxy_initialize_middleware_preserves_legacy_behavior(): + upstream = FastMCP("upstream", instructions="legacy instructions") + + def client_factory() -> ProxyClient: + return ProxyClient(upstream) + + proxy = FastMCPProxy(name="compatibility-proxy", client_factory=client_factory) + + with pytest.warns( + FastMCPDeprecationWarning, + match="`ProxyInitializeMiddleware` is deprecated", + ): + middleware = ProxyInitializeMiddleware(proxy) + + proxy.middleware = [middleware] + async with Client(proxy, mode="legacy") as client: + assert client.instructions == "legacy instructions" + async with Client(proxy, mode="auto") as client: + assert client.instructions is None + + assert middleware.proxy is proxy + + +async def test_fastmcp_proxy_uses_public_metadata_middleware(): + proxy = create_proxy(make_upstream(), name="convenience", identity="upstream") + + assert any( + isinstance(middleware, ProxyMetadataMiddleware) + for middleware in proxy.middleware + ) + async with Client(proxy, mode="auto") as client: + assert client.instructions == "upstream instructions" + assert client.server_info == UPSTREAM_INFO From 803da5319cacf679d75a58a30fe2f069c24a8773 Mon Sep 17 00:00:00 2001 From: "marvin-context-protocol[bot]" <225465937+marvin-context-protocol[bot]@users.noreply.github.com> Date: Thu, 6 Aug 2026 19:56:40 -0400 Subject: [PATCH 48/53] chore: Update SDK documentation (#4679) --- docs/python-sdk-pages.json | 23 + docs/python-sdk/fastmcp-apps-app.mdx | 20 +- docs/python-sdk/fastmcp-apps-config.mdx | 35 +- docs/python-sdk/fastmcp-exceptions.mdx | 36 +- docs/python-sdk/fastmcp-mcp_config.mdx | 38 +- docs/python-sdk/fastmcp-server-caching.mdx | 48 + .../python-sdk/fastmcp-server-completions.mdx | 41 + docs/python-sdk/fastmcp-server-context.mdx | 711 ++++++++++++++ .../fastmcp-server-dependencies.mdx | 614 ++++++++++++ .../python-sdk/fastmcp-server-elicitation.mdx | 152 +++ .../python-sdk/fastmcp-server-event_store.mdx | 78 ++ docs/python-sdk/fastmcp-server-extensions.mdx | 194 ++++ docs/python-sdk/fastmcp-server-http.mdx | 144 +++ docs/python-sdk/fastmcp-server-lifespan.mdx | 101 ++ docs/python-sdk/fastmcp-server-low_level.mdx | 106 +++ docs/python-sdk/fastmcp-server-mixins.mdx | 9 + docs/python-sdk/fastmcp-server-providers.mdx | 34 + docs/python-sdk/fastmcp-server-server.mdx | 891 ++++++++++++++++++ ...tmcp-server-session_scoped_event_store.mdx | 31 + docs/python-sdk/fastmcp-server-sessions.mdx | 319 +++++++ docs/python-sdk/fastmcp-server-telemetry.mdx | 117 +++ docs/python-sdk/fastmcp-server-transforms.mdx | 193 ++++ docs/python-sdk/fastmcp-settings.mdx | 8 +- docs/python-sdk/fastmcp-telemetry.mdx | 70 +- .../fastmcp-utilities-docstring_parsing.mdx | 4 +- .../fastmcp-utilities-exceptions.mdx | 44 +- docs/python-sdk/fastmcp-utilities-inspect.mdx | 10 +- .../fastmcp-utilities-json_schema.mdx | 18 +- docs/python-sdk/fastmcp-utilities-logging.mdx | 6 +- docs/python-sdk/fastmcp-utilities-prefab.mdx | 61 ++ 30 files changed, 4066 insertions(+), 90 deletions(-) create mode 100644 docs/python-sdk/fastmcp-server-caching.mdx create mode 100644 docs/python-sdk/fastmcp-server-completions.mdx create mode 100644 docs/python-sdk/fastmcp-server-context.mdx create mode 100644 docs/python-sdk/fastmcp-server-dependencies.mdx create mode 100644 docs/python-sdk/fastmcp-server-elicitation.mdx create mode 100644 docs/python-sdk/fastmcp-server-event_store.mdx create mode 100644 docs/python-sdk/fastmcp-server-extensions.mdx create mode 100644 docs/python-sdk/fastmcp-server-http.mdx create mode 100644 docs/python-sdk/fastmcp-server-lifespan.mdx create mode 100644 docs/python-sdk/fastmcp-server-low_level.mdx create mode 100644 docs/python-sdk/fastmcp-server-mixins.mdx create mode 100644 docs/python-sdk/fastmcp-server-providers.mdx create mode 100644 docs/python-sdk/fastmcp-server-server.mdx create mode 100644 docs/python-sdk/fastmcp-server-session_scoped_event_store.mdx create mode 100644 docs/python-sdk/fastmcp-server-sessions.mdx create mode 100644 docs/python-sdk/fastmcp-server-telemetry.mdx create mode 100644 docs/python-sdk/fastmcp-server-transforms.mdx create mode 100644 docs/python-sdk/fastmcp-utilities-prefab.mdx diff --git a/docs/python-sdk-pages.json b/docs/python-sdk-pages.json index eb525313d..32abc995c 100644 --- a/docs/python-sdk-pages.json +++ b/docs/python-sdk-pages.json @@ -31,6 +31,28 @@ } ] }, + { + "group": "fastmcp.server", + "pages": [ + "python-sdk/fastmcp-server-caching", + "python-sdk/fastmcp-server-completions", + "python-sdk/fastmcp-server-context", + "python-sdk/fastmcp-server-dependencies", + "python-sdk/fastmcp-server-elicitation", + "python-sdk/fastmcp-server-event_store", + "python-sdk/fastmcp-server-extensions", + "python-sdk/fastmcp-server-http", + "python-sdk/fastmcp-server-lifespan", + "python-sdk/fastmcp-server-low_level", + "python-sdk/fastmcp-server-mixins", + "python-sdk/fastmcp-server-providers", + "python-sdk/fastmcp-server-server", + "python-sdk/fastmcp-server-session_scoped_event_store", + "python-sdk/fastmcp-server-sessions", + "python-sdk/fastmcp-server-telemetry", + "python-sdk/fastmcp-server-transforms" + ] + }, { "group": "fastmcp.utilities", "pages": [ @@ -79,6 +101,7 @@ "python-sdk/fastmcp-utilities-mime", "python-sdk/fastmcp-utilities-openapi", "python-sdk/fastmcp-utilities-pagination", + "python-sdk/fastmcp-utilities-prefab", "python-sdk/fastmcp-utilities-skills", "python-sdk/fastmcp-utilities-tasks", "python-sdk/fastmcp-utilities-tests", diff --git a/docs/python-sdk/fastmcp-apps-app.mdx b/docs/python-sdk/fastmcp-apps-app.mdx index 99dc73528..4d2e8a421 100644 --- a/docs/python-sdk/fastmcp-apps-app.mdx +++ b/docs/python-sdk/fastmcp-apps-app.mdx @@ -35,7 +35,7 @@ Usage:: ## Classes -### `FastMCPApp` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/apps/app.py#L145" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> +### `FastMCPApp` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/apps/app.py#L149" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> A Provider that represents an MCP application. @@ -48,19 +48,19 @@ can find them by original name even when transforms have been applied. **Methods:** -#### `tool` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/apps/app.py#L169" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> +#### `tool` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/apps/app.py#L173" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> ```python tool(self, name_or_fn: F) -> F ``` -#### `tool` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/apps/app.py#L181" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> +#### `tool` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/apps/app.py#L185" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> ```python tool(self, name_or_fn: str | None = None) -> Callable[[F], F] ``` -#### `tool` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/apps/app.py#L192" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> +#### `tool` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/apps/app.py#L196" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> ```python tool(self, name_or_fn: str | AnyFunction | None = None) -> Any @@ -83,19 +83,19 @@ Supports multiple calling patterns:: def save(name: str): ... -#### `ui` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/apps/app.py#L259" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> +#### `ui` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/apps/app.py#L266" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> ```python ui(self, name_or_fn: F) -> F ``` -#### `ui` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/apps/app.py#L274" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> +#### `ui` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/apps/app.py#L281" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> ```python ui(self, name_or_fn: str | None = None) -> Callable[[F], F] ``` -#### `ui` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/apps/app.py#L288" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> +#### `ui` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/apps/app.py#L295" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> ```python ui(self, name_or_fn: str | AnyFunction | None = None) -> Any @@ -119,7 +119,7 @@ Supports multiple calling patterns:: def dashboard() -> Component: ... -#### `add_tool` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/apps/app.py#L363" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> +#### `add_tool` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/apps/app.py#L373" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> ```python add_tool(self, tool: Tool | Callable[..., Any]) -> Tool @@ -130,13 +130,13 @@ Add a tool to this app programmatically. The tool is tagged with this app's name for routing. -#### `lifespan` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/apps/app.py#L419" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> +#### `lifespan` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/apps/app.py#L432" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> ```python lifespan(self) -> AsyncIterator[None] ``` -#### `run` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/apps/app.py#L427" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> +#### `run` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/apps/app.py#L440" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> ```python run(self, transport: Literal['stdio', 'http', 'sse', 'streamable-http'] | None = None, **kwargs: Any) -> None diff --git a/docs/python-sdk/fastmcp-apps-config.mdx b/docs/python-sdk/fastmcp-apps-config.mdx index 9d0c3acf1..a7b9d6151 100644 --- a/docs/python-sdk/fastmcp-apps-config.mdx +++ b/docs/python-sdk/fastmcp-apps-config.mdx @@ -15,7 +15,7 @@ UI metadata for clients that support interactive app rendering. ## Functions -### `app_config_to_meta_dict` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/apps/config.py#L180" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> +### `app_config_to_meta_dict` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/apps/config.py#L181" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> ```python app_config_to_meta_dict(app: AppConfig | dict[str, Any]) -> dict[str, Any] @@ -25,9 +25,32 @@ app_config_to_meta_dict(app: AppConfig | dict[str, Any]) -> dict[str, Any] Convert an AppConfig or dict to the wire-format dict for ``meta["ui"]``. +### `is_model_visible` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/apps/config.py#L188" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +is_model_visible(component: FastMCPComponent) -> bool +``` + + +Whether a component may be shown to, or invoked by, the model. + +Visibility is a declaration, and the MCP Apps spec puts the filtering on +the host — so ``tools/list`` carries app-only tools and the host keeps +them from the model. That division only works where a host stands between +the server and the model. + +It does not hold for surfaces a server drives itself. A search result or +a code-mode catalog reaches the model as ordinary tool output, and a +call-tool proxy invokes on a name the model supplies; nothing downstream +can filter either. Those surfaces have to apply the declaration here. + +A component with no ``visibility`` is visible: the field marks the +exception, and the spec's default is both audiences. + + ## Classes -### `ResourceCSP` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/apps/config.py#L20" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> +### `ResourceCSP` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/apps/config.py#L21" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> Content Security Policy for MCP App resources. @@ -37,7 +60,7 @@ load resources from. Hosts use these declarations to build the ``Content-Security-Policy`` header for the sandboxed iframe. -### `ResourcePermissions` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/apps/config.py#L56" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> +### `ResourcePermissions` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/apps/config.py#L57" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> Iframe sandbox permissions for MCP App resources. @@ -48,7 +71,7 @@ iframe. Hosts MAY honour these; apps should use JS feature detection as a fallback. -### `AppConfig` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/apps/config.py#L84" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> +### `AppConfig` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/apps/config.py#L85" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> Configuration for MCP App tools and resources. @@ -63,7 +86,7 @@ values appear on the wire. Aliases match the MCP Apps wire format (camelCase). -### `PrefabAppConfig` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/apps/config.py#L124" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> +### `PrefabAppConfig` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/apps/config.py#L125" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> App configuration for Prefab tools with sensible defaults. @@ -83,7 +106,7 @@ Example:: **Methods:** -#### `model_post_init` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/apps/config.py#L140" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> +#### `model_post_init` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/apps/config.py#L141" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> ```python model_post_init(self, __context: Any) -> None diff --git a/docs/python-sdk/fastmcp-exceptions.mdx b/docs/python-sdk/fastmcp-exceptions.mdx index a6ef59df9..151f0f10a 100644 --- a/docs/python-sdk/fastmcp-exceptions.mdx +++ b/docs/python-sdk/fastmcp-exceptions.mdx @@ -10,7 +10,7 @@ Custom exceptions for FastMCP. ## Functions -### `to_mcp_error` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/exceptions.py#L122" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> +### `to_mcp_error` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/exceptions.py#L117" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> ```python to_mcp_error(exc: Exception) -> MCPError @@ -38,71 +38,61 @@ explicit code chosen upstream survives translation. ## Classes -### `FastMCPDeprecationWarning` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/exceptions.py#L34" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> - - -Deprecation warning for FastMCP APIs. - -Subclass of DeprecationWarning so that standard warning filters -still apply, but FastMCP can selectively enable its own warnings -without affecting other libraries in the process. - - -### `FastMCPError` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/exceptions.py#L43" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> +### `FastMCPError` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/exceptions.py#L38" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> Base error for FastMCP. -### `ValidationError` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/exceptions.py#L51" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> +### `ValidationError` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/exceptions.py#L46" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> Error in validating parameters or return values. -### `ResourceError` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/exceptions.py#L55" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> +### `ResourceError` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/exceptions.py#L50" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> Error in resource operations. -### `ToolError` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/exceptions.py#L59" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> +### `ToolError` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/exceptions.py#L54" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> Error in tool operations. -### `PromptError` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/exceptions.py#L63" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> +### `PromptError` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/exceptions.py#L58" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> Error in prompt operations. -### `InvalidSignature` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/exceptions.py#L67" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> +### `InvalidSignature` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/exceptions.py#L62" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> Invalid signature for use with FastMCP. -### `ClientError` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/exceptions.py#L71" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> +### `ClientError` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/exceptions.py#L66" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> Error in client operations. -### `NotFoundError` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/exceptions.py#L75" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> +### `NotFoundError` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/exceptions.py#L70" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> Object not found. -### `DisabledError` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/exceptions.py#L79" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> +### `DisabledError` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/exceptions.py#L74" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> Object is disabled. -### `ResourceSecurityError` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/exceptions.py#L83" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> +### `ResourceSecurityError` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/exceptions.py#L78" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> A templated resource parameter failed path-security screening. @@ -114,13 +104,13 @@ for a resource that does not exist, and never reveals which parameter or policy tripped. -### `AuthorizationError` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/exceptions.py#L94" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> +### `AuthorizationError` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/exceptions.py#L89" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> Error when authorization check fails. -### `InsufficientScopeError` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/exceptions.py#L98" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> +### `InsufficientScopeError` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/exceptions.py#L93" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> Authorization failed because the token is missing required OAuth scopes. diff --git a/docs/python-sdk/fastmcp-mcp_config.mdx b/docs/python-sdk/fastmcp-mcp_config.mdx index 44e287d46..70f0978ff 100644 --- a/docs/python-sdk/fastmcp-mcp_config.mdx +++ b/docs/python-sdk/fastmcp-mcp_config.mdx @@ -32,7 +32,7 @@ Example configuration: ## Functions -### `infer_transport_type_from_url` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/mcp_config.py#L54" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> +### `infer_transport_type_from_url` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/mcp_config.py#L55" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> ```python infer_transport_type_from_url(url: str | AnyUrl) -> Literal['http', 'sse'] @@ -42,7 +42,7 @@ infer_transport_type_from_url(url: str | AnyUrl) -> Literal['http', 'sse'] Infer the appropriate transport type from the given URL. -### `update_config_file` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/mcp_config.py#L373" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> +### `update_config_file` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/mcp_config.py#L376" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> ```python update_config_file(file_path: Path, server_name: str, server_config: CanonicalMCPServerTypes) -> None @@ -57,7 +57,7 @@ worry about transforming server objects here. ## Classes -### `StdioMCPServer` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/mcp_config.py#L179" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> +### `StdioMCPServer` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/mcp_config.py#L180" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> MCP server configuration for stdio transport. @@ -67,19 +67,19 @@ This is the canonical configuration format for MCP servers using stdio transport **Methods:** -#### `to_transport` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/mcp_config.py#L212" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> +#### `to_transport` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/mcp_config.py#L213" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> ```python -to_transport(self) -> StdioTransport +to_transport(self) -> StdioTransport | FastMCPTransport ``` -### `TransformingStdioMCPServer` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/mcp_config.py#L224" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> +### `TransformingStdioMCPServer` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/mcp_config.py#L225" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> A Stdio server with tool transforms. -### `RemoteMCPServer` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/mcp_config.py#L228" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> +### `RemoteMCPServer` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/mcp_config.py#L229" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> MCP server configuration for HTTP/SSE transport. @@ -89,19 +89,19 @@ This is the canonical configuration format for MCP servers using remote transpor **Methods:** -#### `to_transport` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/mcp_config.py#L264" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> +#### `to_transport` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/mcp_config.py#L265" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> ```python -to_transport(self) -> StreamableHttpTransport | SSETransport +to_transport(self) -> StreamableHttpTransport | SSETransport | FastMCPTransport ``` -### `TransformingRemoteMCPServer` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/mcp_config.py#L291" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> +### `TransformingRemoteMCPServer` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/mcp_config.py#L294" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> A Remote server with tool transforms. -### `MCPConfig` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/mcp_config.py#L302" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> +### `MCPConfig` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/mcp_config.py#L305" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> A configuration object for MCP Servers that conforms to the canonical MCP configuration format @@ -113,7 +113,7 @@ For an MCPConfig that is strictly canonical, see the `CanonicalMCPConfig` class. **Methods:** -#### `wrap_servers_at_root` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/mcp_config.py#L316" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> +#### `wrap_servers_at_root` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/mcp_config.py#L319" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> ```python wrap_servers_at_root(cls, values: dict[str, Any]) -> dict[str, Any] @@ -122,7 +122,7 @@ wrap_servers_at_root(cls, values: dict[str, Any]) -> dict[str, Any] If there's no mcpServers key but there are server configs at root, wrap them. -#### `add_server` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/mcp_config.py#L329" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> +#### `add_server` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/mcp_config.py#L332" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> ```python add_server(self, name: str, server: MCPServerTypes) -> None @@ -131,7 +131,7 @@ add_server(self, name: str, server: MCPServerTypes) -> None Add or update a server in the configuration. -#### `from_dict` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/mcp_config.py#L334" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> +#### `from_dict` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/mcp_config.py#L337" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> ```python from_dict(cls, config: dict[str, Any]) -> Self @@ -140,7 +140,7 @@ from_dict(cls, config: dict[str, Any]) -> Self Parse MCP configuration from dictionary format. -#### `to_dict` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/mcp_config.py#L338" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> +#### `to_dict` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/mcp_config.py#L341" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> ```python to_dict(self) -> dict[str, Any] @@ -149,7 +149,7 @@ to_dict(self) -> dict[str, Any] Convert MCPConfig to dictionary format, preserving all fields. -#### `write_to_file` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/mcp_config.py#L342" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> +#### `write_to_file` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/mcp_config.py#L345" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> ```python write_to_file(self, file_path: Path) -> None @@ -158,7 +158,7 @@ write_to_file(self, file_path: Path) -> None Write configuration to JSON file. -#### `from_file` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/mcp_config.py#L348" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> +#### `from_file` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/mcp_config.py#L351" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> ```python from_file(cls, file_path: Path) -> Self @@ -167,7 +167,7 @@ from_file(cls, file_path: Path) -> Self Load configuration from JSON file. -### `CanonicalMCPConfig` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/mcp_config.py#L358" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> +### `CanonicalMCPConfig` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/mcp_config.py#L361" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> Canonical MCP configuration format. @@ -178,7 +178,7 @@ The format is designed to be client-agnostic and extensible for future use cases **Methods:** -#### `add_server` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/mcp_config.py#L368" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> +#### `add_server` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/mcp_config.py#L371" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> ```python add_server(self, name: str, server: CanonicalMCPServerTypes) -> None diff --git a/docs/python-sdk/fastmcp-server-caching.mdx b/docs/python-sdk/fastmcp-server-caching.mdx new file mode 100644 index 000000000..d4e76a033 --- /dev/null +++ b/docs/python-sdk/fastmcp-server-caching.mdx @@ -0,0 +1,48 @@ +--- +title: caching +sidebarTitle: caching +--- + +# `fastmcp.server.caching` + + +Server-level cache hints for FastMCP (SEP-2549). + +A FastMCP server opts every SDK-cacheable result it emits into client-side +caching by setting `cache_ttl` (seconds) and, optionally, `cache_scope` on the +`FastMCP` constructor. The hint is uniform by construction: one server-level +value applies to `tools/list`, `prompts/list`, `resources/list`, +`resources/templates/list`, `resources/read`, and `server/discover` alike — no +per-component surface and no aggregation. + +FastMCP does not hand-set the wire fields. It passes the hint through to the SDK +low-level `Server(cache_hints=...)`, whose runner fills `ttlMs`/`cacheScope` on +every cacheable result via `apply_cache_hint`, leaving any field a handler set +explicitly untouched. Honoring is modern-only and opt-in on the client: a hinted +server is inert unless the client passes `cache=` and negotiates `2026-07-28`. + + +## Functions + +### `build_cache_hints` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/caching.py#L29" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +build_cache_hints(cache_ttl: int | None, cache_scope: CacheScope | None) -> dict[CacheableMethod, CacheHint] | None +``` + + +Build the per-method `CacheHint` map for the SDK low-level server. + +`cache_ttl` is in seconds and is converted to the wire's milliseconds. When +`cache_ttl` is `None` the server emits no hint, so its wire output is +identical to a server that never set one; a `cache_scope` given without a +`cache_ttl` is meaningless (the client gates caching on the presence of a +TTL) and is rejected rather than silently ignored. + +Returns `None` when no hint is set, or a map applying the same hint to every +SDK-cacheable method otherwise. + +**Raises:** +- `ValueError`: If `cache_ttl` is not positive, or if `cache_scope` is set +without `cache_ttl`. + diff --git a/docs/python-sdk/fastmcp-server-completions.mdx b/docs/python-sdk/fastmcp-server-completions.mdx new file mode 100644 index 000000000..dcea2c00d --- /dev/null +++ b/docs/python-sdk/fastmcp-server-completions.mdx @@ -0,0 +1,41 @@ +--- +title: completions +sidebarTitle: completions +--- + +# `fastmcp.server.completions` + + +Server-side argument completion for FastMCP. + +A completion request names a reference — a specific prompt or resource +template — and the argument being completed, plus a context of the argument +values already supplied. The server answers with candidate string values. + +FastMCP surfaces this as a single server-level handler registered with +``@mcp.completion``, mirroring the MCP SDK's own ``completion/complete`` shape +and FastMCP's client-side ``Client.complete()``. The handler receives the +reference, the argument, and the optional context, and returns candidates for +whichever reference/argument pair it recognizes. + + +## Functions + +### `normalize_completion` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/completions.py#L54" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +normalize_completion(result: CompletionValues) -> mcp_types.Completion +``` + + +Coerce a handler's return value into a wire ``Completion``. + +A returned ``str`` is rejected: it is almost always a mistake (the value +would iterate into one-character candidates), so it raises rather than +silently producing surprising output. + +The MCP contract caps a completion at 100 values, so a longer result is +truncated to the first 100 with ``has_more`` set — a handler that returns +thousands of matches emits a conforming response rather than an oversized +one that strict clients reject. + diff --git a/docs/python-sdk/fastmcp-server-context.mdx b/docs/python-sdk/fastmcp-server-context.mdx new file mode 100644 index 000000000..a9d766b6b --- /dev/null +++ b/docs/python-sdk/fastmcp-server-context.mdx @@ -0,0 +1,711 @@ +--- +title: context +sidebarTitle: context +--- + +# `fastmcp.server.context` + +## Functions + +### `set_transport` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/context.py#L97" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +set_transport(transport: TransportType) -> Token[TransportType | None] +``` + + +Set the current transport type. Returns token for reset. + + +### `reset_transport` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/context.py#L104" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +reset_transport(token: Token[TransportType | None]) -> None +``` + + +Reset transport to previous value. + + +### `set_context` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/context.py#L134" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +set_context(context: Context) -> Generator[Context, None, None] +``` + +## Classes + +### `LogData` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/context.py#L110" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + + +Data object for passing log arguments to client-side handlers. + +This provides an interface to match the Python standard library logging, +for compatibility with structured logging. + + +### `Context` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/context.py#L143" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + + +Context object providing access to MCP capabilities. + +This provides a cleaner interface to MCP's RequestContext functionality. +It gets injected into tool and resource functions that request it via type hints. + +To use context in a tool function, add a parameter with the Context type annotation: + +```python +@server.tool +async def my_tool(x: int, ctx: Context) -> str: + # Log messages to the client + await ctx.info(f"Processing {x}") + await ctx.debug("Debug info") + await ctx.warning("Warning message") + await ctx.error("Error message") + + # Report progress + await ctx.report_progress(50, 100, "Processing") + + # Access resources + data = await ctx.read_resource("resource://data") + + # Get request info + request_id = ctx.request_id + client_id = ctx.client_id + + # Manage state across the session (persists across requests) + await ctx.set_state("key", "value") + value = await ctx.get_state("key") + + # Store non-serializable values for the current request only + await ctx.set_state("client", http_client, serializable=False) + + return str(x) +``` + +State Management: +Context provides session-scoped state that persists across requests within +the same MCP session. State is automatically keyed by session, ensuring +isolation between different clients. + +State set during `on_initialize` middleware will persist to subsequent tool +calls when using the same session object (STDIO, SSE, single-server HTTP). +For distributed/serverless HTTP deployments where different machines handle +the init and tool calls, state is isolated by the mcp-session-id header. + +The context parameter name can be anything as long as it's annotated with Context. +The context is optional - tools that don't need it can omit the parameter. + + +**Methods:** + +#### `is_background_task` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/context.py#L224" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +is_background_task(self) -> bool +``` + +True when this context is running in a background task (Docket worker). + +When True, certain operations like elicit() will use task-aware +implementations that can pause the task and wait for client input. + + +#### `task_id` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/context.py#L242" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +task_id(self) -> str | None +``` + +Get the background task ID if running in a background task. + +Returns None if not running in a background task context. + + +#### `origin_request_id` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/context.py#L250" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +origin_request_id(self) -> str | None +``` + +Get the request ID that originated this execution, if available. + +In foreground request mode, this is the current request_id. +In background task mode, this is the request_id captured when the task +was submitted, if one was available. + + +#### `fastmcp` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/context.py#L262" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +fastmcp(self) -> FastMCP +``` + +Get the FastMCP instance. + + +#### `request_context` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/context.py#L312" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +request_context(self) -> FastMCPRequestContext | None +``` + +Access to the underlying request context. + +Returns None when the MCP session has not been established yet. +Returns the FastMCPRequestContext wrapper once the MCP session is available. + +For HTTP request access in middleware, use `get_http_request()` from fastmcp.server.dependencies, +which works whether or not the MCP session is available. + +Example in middleware: +```python +async def on_request(self, context, call_next): + ctx = context.fastmcp_context + if ctx.request_context: + # MCP session available - can access session_id, request_id, etc. + session_id = ctx.session_id + else: + # MCP session not available yet - use HTTP helpers + from fastmcp.server.dependencies import get_http_request + request = get_http_request() + return await call_next(context) +``` + + +#### `client_extension_settings` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/context.py#L337" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +client_extension_settings(self, identifier: str) -> dict[str, Any] | None +``` + +This request's per-request opt-in settings for an MCP extension. + +SEP-2133 extensions negotiate per request: the client repeats its +extension capabilities in each request's ``_meta`` under +``io.modelcontextprotocol/clientCapabilities`` → ``extensions`` → +``identifier``. Returns the declared settings dict (possibly empty) when +the extension was opted in for this request, or ``None`` when it was +not (or there is no active request). This bridges an extension's +``tools/call`` interceptor — which receives a FastMCP ``Context`` — to +the request's declared client capabilities. + + +#### `input_responses` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/context.py#L378" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +input_responses(self) -> mcp_types.InputResponses | None +``` + +Client responses to a prior `InputRequiredResult.input_requests`. + +The multi-round-trip guard channel (SEP-2322). A guard tool inspects +this to decide what to do on each round: `None` on the initial round +(nothing has been asked yet, or the client retried without responses), +so the tool returns an `InputRequiredResult` to ask; present on a later +round, so the tool reads the answers and proceeds. It is a mapping whose +keys match the `input_requests` map the tool minted; each value is the +client's result for that request (an `ElicitResult`, `CreateMessageResult`, +or `ListRootsResult`). + +In a background task there is no wire request, so this falls back to the +responses the in-task guard loop delivered (see the tasks extension). + + +#### `request_state` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/context.py#L399" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +request_state(self) -> str | None +``` + +Opaque state echoed from a prior `InputRequiredResult.request_state`. + +The multi-round-trip guard channel (SEP-2322): whatever a tool put in +`InputRequiredResult.request_state` on an earlier round is handed back +here (as plaintext — the framework seals it on the wire and unseals it +before the tool runs, so tampering is rejected before this is read). +`None` on the initial round. Use it to carry a small amount of computed +state across rounds without re-deriving it. + +In a background task there is no wire request, so this falls back to the +state the in-task guard loop re-injected (see the tasks extension). + + +#### `lifespan_context` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/context.py#L418" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +lifespan_context(self) -> dict[str, Any] +``` + +Access the server's lifespan context. + +Returns the context dict yielded by *this* server's lifespan function. +For a mounted child this is the child's own lifespan, not the parent's +— the MCP session always belongs to the parent, so reading from the +request context would return the parent's. We read directly from the +server's cached lifespan result instead, which is set by the +per-server ``_lifespan_manager`` regardless of mount position. + +Returns an empty dict if no lifespan was configured. + +Example: +```python +@server.tool +def my_tool(ctx: Context) -> str: + db = ctx.lifespan_context.get("db") + if db: + return db.query("SELECT 1") + return "No database connection" +``` + + +#### `report_progress` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/context.py#L453" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +report_progress(self, progress: float, total: float | None = None, message: str | None = None) -> None +``` + +Report progress for the current operation. + +Works in both foreground (MCP progress notifications) and background +(Docket task execution) contexts. + +**Args:** +- `progress`: Current progress value e.g. 24 +- `total`: Optional total value e.g. 100 +- `message`: Optional status message describing current progress + + +#### `list_resources` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/context.py#L552" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +list_resources(self) -> list[SDKResource] +``` + +List all available resources from the server. + +**Returns:** +- List of Resource objects available on the server + + +#### `list_prompts` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/context.py#L563" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +list_prompts(self) -> list[SDKPrompt] +``` + +List all available prompts from the server. + +**Returns:** +- List of Prompt objects available on the server + + +#### `get_prompt` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/context.py#L574" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +get_prompt(self, name: str, arguments: dict[str, Any] | None = None) -> GetPromptResult +``` + +Get a prompt by name with optional arguments. + +**Args:** +- `name`: The name of the prompt to get +- `arguments`: Optional arguments to pass to the prompt + +**Returns:** +- The prompt result + + +#### `read_resource` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/context.py#L593" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +read_resource(self, uri: str | AnyUrl) -> ResourceResult +``` + +Read a resource by URI. + +**Args:** +- `uri`: Resource URI to read + +**Returns:** +- ResourceResult with contents + + +#### `log` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/context.py#L609" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +log(self, message: str, level: LoggingLevel | None = None, logger_name: str | None = None, extra: Mapping[str, Any] | None = None) -> None +``` + +Send a log message to the client. + +Messages sent to Clients are also logged to the `fastmcp.server.context.to_client` logger with a level of `DEBUG`. + +**Args:** +- `message`: Log message +- `level`: Optional log level. One of "debug", "info", "notice", "warning", "error", "critical", +"alert", or "emergency". Default is "info". +- `logger_name`: Optional logger name +- `extra`: Optional mapping for additional arguments + + +#### `transport` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/context.py#L650" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +transport(self) -> TransportType | None +``` + +Get the current transport type. + +Returns the transport type used to run this server: "stdio", "sse", +or "streamable-http". Returns None if called outside of a server context. + + +#### `client_supports_extension` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/context.py#L658" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +client_supports_extension(self, extension_id: str) -> bool +``` + +Check whether the connected client supports a given MCP extension. + +Inspects the ``extensions`` extra field on ``ClientCapabilities`` +sent by the client during initialization. + +Reads the client's advertised capabilities from the session, which is +available in request mode and in background-task mode (where the +snapshot session preserves the client's initialize params). Returns +``False`` when no session is available (e.g., a distributed worker with +no live session, or outside any context) or when the client did not +advertise the extension. + +Example:: + + from fastmcp.apps.config import UI_EXTENSION_ID + + @mcp.tool + async def my_tool(ctx: Context) -> str: + if ctx.client_supports_extension(UI_EXTENSION_ID): + return "UI-capable client" + return "text-only client" + + +#### `client_id` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/context.py#L688" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +client_id(self) -> str | None +``` + +Get the client ID if available. + + +#### `request_id` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/context.py#L696" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +request_id(self) -> str +``` + +Get the unique ID for this request. + +Raises RuntimeError if MCP request context is not available. + + +#### `session_id` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/context.py#L709" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +session_id(self) -> str +``` + +Get the MCP session ID for ALL transports. + +Returns the session ID that can be used as a key for session-based +data storage (e.g., Redis) to share data between tool calls within +the same client session. + +**Returns:** +- The session ID for StreamableHTTP transports, or a generated ID +- for other transports. + + +#### `session` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/context.py#L794" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +session(self) -> ServerSession +``` + +Access to the underlying session for advanced usage. + +In request mode: Returns the session from the active request context. +In background task mode: Returns the session stored at Context creation. + +Raises RuntimeError if no session is available. + + +#### `debug` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/context.py#L820" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +debug(self, message: str, logger_name: str | None = None, extra: Mapping[str, Any] | None = None) -> None +``` + +Send a `DEBUG`-level message to the connected MCP Client. + +Messages sent to Clients are also logged to the `fastmcp.server.context.to_client` logger with a level of `DEBUG`. + + +#### `info` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/context.py#L836" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +info(self, message: str, logger_name: str | None = None, extra: Mapping[str, Any] | None = None) -> None +``` + +Send a `INFO`-level message to the connected MCP Client. + +Messages sent to Clients are also logged to the `fastmcp.server.context.to_client` logger with a level of `DEBUG`. + + +#### `warning` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/context.py#L852" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +warning(self, message: str, logger_name: str | None = None, extra: Mapping[str, Any] | None = None) -> None +``` + +Send a `WARNING`-level message to the connected MCP Client. + +Messages sent to Clients are also logged to the `fastmcp.server.context.to_client` logger with a level of `DEBUG`. + + +#### `error` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/context.py#L868" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +error(self, message: str, logger_name: str | None = None, extra: Mapping[str, Any] | None = None) -> None +``` + +Send a `ERROR`-level message to the connected MCP Client. + +Messages sent to Clients are also logged to the `fastmcp.server.context.to_client` logger with a level of `DEBUG`. + + +#### `send_notification` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/context.py#L884" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +send_notification(self, notification: mcp_types.ServerNotification) -> None +``` + +Send a notification to the client immediately. + +**Args:** +- `notification`: An MCP notification instance (e.g., ToolListChangedNotification()) + + +#### `close_sse_stream` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/context.py#L904" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +close_sse_stream(self) -> None +``` + +Close the current response stream to trigger client reconnection. + +When using StreamableHTTP transport with an EventStore configured, this +method gracefully closes the HTTP connection for the current request. +The client will automatically reconnect (after `retry_interval` milliseconds) +and resume receiving events from where it left off via the EventStore. + +This is useful for long-running operations to avoid load balancer timeouts. +Instead of holding a connection open for minutes, you can periodically close +and let the client reconnect. + + +#### `elicit` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/context.py#L958" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +elicit(self, message: str, response_type: type[T]) -> AcceptedElicitation[T] | DeclinedElicitation | CancelledElicitation +``` + +The accepted elicitation will contain the response data + + +#### `elicit` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/context.py#L969" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +elicit(self, message: str, response_type: list[str]) -> AcceptedElicitation[str] | DeclinedElicitation | CancelledElicitation +``` + +When response_type is a list of strings, the accepted elicitation will +contain the selected string response + + +#### `elicit` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/context.py#L981" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +elicit(self, message: str, response_type: dict[str, dict[str, str]]) -> AcceptedElicitation[str] | DeclinedElicitation | CancelledElicitation +``` + +When response_type is a dict mapping keys to title dicts, the accepted +elicitation will contain the selected key + + +#### `elicit` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/context.py#L993" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +elicit(self, message: str, response_type: list[list[str]]) -> AcceptedElicitation[list[str]] | DeclinedElicitation | CancelledElicitation +``` + +When response_type is a list containing a list of strings (multi-select), +the accepted elicitation will contain a list of selected strings + + +#### `elicit` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/context.py#L1005" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +elicit(self, message: str, response_type: list[dict[str, dict[str, str]]]) -> AcceptedElicitation[list[str]] | DeclinedElicitation | CancelledElicitation +``` + +When response_type is a list containing a dict mapping keys to title dicts +(multi-select with titles), the accepted elicitation will contain a list of +selected keys + + +#### `elicit` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/context.py#L1017" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +elicit(self, message: str, response_type: type[T] | list[str] | dict[str, dict[str, str]] | list[list[str]] | list[dict[str, dict[str, str]]]) -> AcceptedElicitation[T] | AcceptedElicitation[dict[str, Any]] | AcceptedElicitation[str] | AcceptedElicitation[list[str]] | DeclinedElicitation | CancelledElicitation +``` + +Send an elicitation request to the client and await the response. + +Call this method at any time to request additional information from +the user through the client. The client must support elicitation, +or the request will error. + +Note that the MCP protocol only supports simple object schemas with +primitive types. You can provide a dataclass, TypedDict, or BaseModel to +comply. If you provide a primitive type, an object schema with a single +"value" field will be generated for the MCP interaction and +automatically deconstructed into the primitive type upon response. + +``response_type`` is required. Pass ``bool`` when all you need is a +confirmation; an empty schema leaves some clients rendering an empty, +non-functional form. + +**Args:** +- `message`: A human-readable message explaining what information is needed +- `response_type`: The type of the response, which should be a primitive +type or dataclass or BaseModel. If it is a primitive type, an +object schema with a single "value" field will be generated. +- `response_title`: Optional label to display for the wrapped ``value`` +field when ``response_type`` is a scalar, Literal, Enum, or one +of the dict/list shorthand forms. Overrides the auto-generated +"Value" label. Raises ``TypeError`` if passed with a BaseModel, +dataclass, or ``None`` response type (use ``Field(title=...)`` +on the model instead). +- `response_description`: Optional description to attach to the wrapped +``value`` field. Same scope rules as ``response_title``. + + +#### `set_state` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/context.py#L1110" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +set_state(self, key: str, value: Any) -> None +``` + +Set a value in the state store. + +By default, values are stored in the session-scoped state store and +persist across requests within the same MCP session. Values must be +JSON-serializable (dicts, lists, strings, numbers, etc.). + +For non-serializable values (e.g., HTTP clients, database connections), +pass ``serializable=False``. These values are stored in a request-scoped +dict and only live for the current MCP request (tool call, resource +read, or prompt render). They will not be available in subsequent +requests. + +The key is automatically prefixed with the session identifier. + + +#### `get_state` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/context.py#L1164" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +get_state(self, key: str) -> Any +``` + +Get a value from the state store. + +Checks request-scoped state first (set with ``serializable=False``), +then falls back to the session-scoped state store. + +Returns None if the key is not found. + + +#### `delete_state` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/context.py#L1178" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +delete_state(self, key: str) -> None +``` + +Delete a value from the state store. + +Removes from both request-scoped and session-scoped stores. + + +#### `enable_components` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/context.py#L1199" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +enable_components(self) -> None +``` + +Enable components matching criteria for this session only. + +Session rules override global transforms. Rules accumulate - each call +adds a new rule to the session. Later marks override earlier ones +(Visibility transform semantics). + +Sends notifications to this session only: ToolListChangedNotification, +ResourceListChangedNotification, and PromptListChangedNotification. + +**Args:** +- `names`: Component names or URIs to match. +- `keys`: Component keys to match (e.g., {"tool\:my_tool@v1"}). +- `version`: Component version spec to match. +- `tags`: Tags to match (component must have at least one). +- `components`: Component types to match (e.g., {"tool", "prompt"}). +- `match_all`: If True, matches all components regardless of other criteria. + + +#### `disable_components` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/context.py#L1237" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +disable_components(self) -> None +``` + +Disable components matching criteria for this session only. + +Session rules override global transforms. Rules accumulate - each call +adds a new rule to the session. Later marks override earlier ones +(Visibility transform semantics). + +Sends notifications to this session only: ToolListChangedNotification, +ResourceListChangedNotification, and PromptListChangedNotification. + +**Args:** +- `names`: Component names or URIs to match. +- `keys`: Component keys to match (e.g., {"tool\:my_tool@v1"}). +- `version`: Component version spec to match. +- `tags`: Tags to match (component must have at least one). +- `components`: Component types to match (e.g., {"tool", "prompt"}). +- `match_all`: If True, matches all components regardless of other criteria. + + +#### `reset_visibility` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/context.py#L1275" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +reset_visibility(self) -> None +``` + +Clear all session visibility rules. + +Use this to reset session visibility back to global defaults. + +Sends notifications to this session only: ToolListChangedNotification, +ResourceListChangedNotification, and PromptListChangedNotification. + diff --git a/docs/python-sdk/fastmcp-server-dependencies.mdx b/docs/python-sdk/fastmcp-server-dependencies.mdx new file mode 100644 index 000000000..1ab291fb1 --- /dev/null +++ b/docs/python-sdk/fastmcp-server-dependencies.mdx @@ -0,0 +1,614 @@ +--- +title: dependencies +sidebarTitle: dependencies +--- + +# `fastmcp.server.dependencies` + + +Dependency injection for FastMCP. + +DI features (Depends, CurrentContext, CurrentFastMCP) work without pydocket +using the uncalled-for DI engine. The docket-specific dependencies +(``CurrentDocket``, ``CurrentWorker``) and background task execution live in the +``fastmcp-tasks`` package. + + +## Functions + +### `bind_request_context` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/dependencies.py#L104" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +bind_request_context(ctx: ServerRequestContext) -> Generator[FastMCPRequestContext, None, None] +``` + + +Bind a ``FastMCPRequestContext`` for the duration of a handler. + +Constructs the wrapper from the SDK's per-request context and sets/resets +the ``fastmcp_request_ctx`` ContextVar. Every request adapter and the +initialize middleware enters this so ``Context`` and dependency helpers can +read the active request from the ContextVar. + + +### `extract_version_spec` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/dependencies.py#L131" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +extract_version_spec(meta: dict[str, Any] | None) -> str | None +``` + + +Extract the FastMCP component version from a lifted ``_meta`` block. + + +### `set_background_context_factory` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/dependencies.py#L185" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +set_background_context_factory(factory: Callable[[], Awaitable[Context | None]] | None) -> None +``` + + +Install (or clear) the background-task ``Context`` factory. + +The factory returns an already-entered ``Context`` (so ``_current_context`` +is set for cleanup) when called inside a worker, or ``None`` when there is +no task context. Passing ``None`` restores core's no-worker-fallback +behavior. + + +### `set_worker_server_resolver` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/dependencies.py#L207" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +set_worker_server_resolver(resolver: Callable[[], FastMCP | None] | None) -> None +``` + + +Install (or clear) the worker-server resolver used by ``get_server()``. + + +### `is_docket_available` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/dependencies.py#L244" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +is_docket_available() -> bool +``` + + +Check if a compatible pydocket (>= 0.19.0) is installed and importable. + +Three things have to be true for fastmcp's task features to work: + 1. pydocket distribution metadata is discoverable + 2. its version is at least ``_MIN_DOCKET_VERSION`` (older versions are + missing symbols like ``docket.dependencies.current_execution``, + which fastmcp imports on the request hot path) + 3. the package actually imports — guards against broken/partial + installs where metadata exists but ``import docket`` blows up + +Any of those failing means we treat docket as unavailable and fall back +to the no-tasks code paths instead of crashing deep inside a request. + + +### `transform_context_annotations` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/dependencies.py#L276" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +transform_context_annotations(fn: Callable[..., Any]) -> Callable[..., Any] +``` + + +Transform injected-by-type params into Dependency-defaulted params. + +Transforms ALL params typed as Context (into ``= CurrentContext()``) and as +UserSession (into ``= CurrentSession()``) to use Docket's DI system, unless +they already have a Dependency-based default. + +This unifies the legacy type annotation DI with Docket's Depends() system, +allowing both patterns to work through a single resolution path. + +Note: Only POSITIONAL_OR_KEYWORD parameters are reordered (params with defaults +after those without). KEYWORD_ONLY parameters keep their position since Python +allows them to have defaults in any order. + +**Args:** +- `fn`: Function to transform + +**Returns:** +- Function with modified signature (same function object, updated __signature__) + + +### `get_context` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/dependencies.py#L442" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +get_context() -> Context +``` + + +Get the current FastMCP Context instance directly. + + +### `get_server` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/dependencies.py#L452" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +get_server() -> FastMCP +``` + + +Get the current FastMCP server instance directly. + +In a background-task worker the tasks extension's resolver is consulted +first, so a mounted-child task resolves to the child server rather than the +root that started the worker (#3571). + +**Returns:** +- The active FastMCP server + +**Raises:** +- `RuntimeError`: If no server in context + + +### `get_session` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/dependencies.py#L480" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +get_session(session_id: str) -> Session +``` + + +Resolve and validate a `Session` for an explicit `session_id`. + +Pair with a `session_id: SessionId` tool argument (the agent obtains an id +from `create_session` and passes it back). For a single per-user bucket with +nothing for the agent to pass, inject `session: UserSession` instead. + +State is keyed by `(principal, session_id)`: the authenticated principal is +the isolation wall and `session_id` organizes sessions within it. The id must +have been minted by `create_session` under the current principal; an id that +was never created, or created under a different principal, raises +`InvalidSession` rather than resolving to a fresh empty bucket (the specific +reason is logged at debug level, never returned to the caller). + +Like `get_server()`, this resolves through the task-aware server, so it needs +no foreground context — it works from a `task=True` tool's Docket worker as +well as a normal request. + + +### `get_http_request` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/dependencies.py#L515" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +get_http_request() -> Request +``` + + +Get the current HTTP request. + +Tries MCP SDK's request_ctx first, then falls back to FastMCP's HTTP context. + + +### `get_http_headers` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/dependencies.py#L536" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +get_http_headers(include_all: bool = False, include: set[str] | None = None) -> dict[str, str] +``` + + +Extract headers from the current HTTP request if available. + +Never raises an exception, even if there is no active HTTP request (in which case +an empty dict is returned). + +By default, strips problematic headers like `content-length` and `authorization` +that cause issues if forwarded to downstream services. If `include_all` is True, +all headers are returned. + +The `include` parameter allows specific headers to be included even if they would +normally be excluded. This is useful for proxy transports that need to forward +authorization headers to upstream MCP servers. + + +### `get_access_token` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/dependencies.py#L600" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +get_access_token() -> AccessToken | None +``` + + +Get the FastMCP access token from the current context. + +This function first tries to get the token from the current HTTP request's scope, +which is more reliable for long-lived connections where the SDK's auth_context_var +may become stale after token refresh. Falls back to the SDK's context var if no +request is available. + +**Returns:** +- The access token if an authenticated user is available, None otherwise. + + +### `without_injected_parameters` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/dependencies.py#L659" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +without_injected_parameters(fn: Callable[..., Any]) -> Callable[..., Any] +``` + + +Create a wrapper function without injected parameters. + +Returns a wrapper that excludes Context and Docket dependency parameters, +making it safe to use with Pydantic TypeAdapter for schema generation and +validation. The wrapper internally handles all dependency resolution and +Context injection when called. + +Handles: +- Legacy Context injection (always works) +- Depends() injection (always works - uses docket or vendored DI engine) + +**Args:** +- `fn`: Original function with Context and/or dependencies +- `run_in_thread`: For sync ``fn``, whether to dispatch the call to a worker +thread after resolving dependencies. Defaults to True. Set to False +to call ``fn`` inline on the event loop thread — required for +thread-affinity libraries (e.g. Windows COM). Ignored for async fns. + +**Returns:** +- Async wrapper function without injected parameters + + +### `resolve_dependencies` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/dependencies.py#L820" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +resolve_dependencies(fn: Callable[..., Any], arguments: dict[str, Any]) -> AsyncGenerator[dict[str, Any], None] +``` + + +Resolve dependencies for a FastMCP function. + +This function: +1. Filters out any dependency parameter names from user arguments (security) +2. Resolves Depends() parameters via the DI system + +The filtering prevents external callers from overriding injected parameters by +providing values for dependency parameter names. This is a security feature. + +Note: Context injection is handled via transform_context_annotations() which +converts `ctx: Context` to `ctx: Context = Depends(get_context)` at registration +time, so all injection goes through the unified DI system. + +**Args:** +- `fn`: The function to resolve dependencies for +- `arguments`: User arguments (may contain keys that match dependency names, + which will be filtered out) + + +### `CurrentContext` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/dependencies.py#L945" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +CurrentContext() -> Context +``` + + +Get the current FastMCP Context instance. + +This dependency provides access to the active FastMCP Context for the +current MCP operation (tool/resource/prompt call). + +**Returns:** +- A dependency that resolves to the active Context instance + +**Raises:** +- `RuntimeError`: If no active context found (during resolution) + + +### `OptionalCurrentContext` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/dependencies.py#L970" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +OptionalCurrentContext() -> Context | None +``` + + +Get the current FastMCP Context, or None when no context is active. + + +### `CurrentFastMCP` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/dependencies.py#L990" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +CurrentFastMCP() -> FastMCP +``` + + +Get the current FastMCP server instance. + +This dependency provides access to the active FastMCP server. + +**Returns:** +- A dependency that resolves to the active FastMCP server + +**Raises:** +- `RuntimeError`: If no server in context (during resolution) + + +### `CurrentRequest` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/dependencies.py#L1030" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +CurrentRequest() -> Request +``` + + +Get the current HTTP request. + +This dependency provides access to the Starlette Request object for the +current HTTP request. Only available when running over HTTP transports +(SSE or Streamable HTTP). + +**Returns:** +- A dependency that resolves to the active Starlette Request + +**Raises:** +- `RuntimeError`: If no HTTP request in context (e.g., STDIO transport) + + +### `CurrentHeaders` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/dependencies.py#L1071" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +CurrentHeaders() -> dict[str, str] +``` + + +Get the current HTTP request headers. + +This dependency provides access to the HTTP headers for the current request, +including the authorization header. Returns an empty dictionary when no HTTP +request is available, making it safe to use in code that might run over any +transport. + +**Returns:** +- A dependency that resolves to a dictionary of header name -> value + + +### `CurrentAccessToken` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/dependencies.py#L1289" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +CurrentAccessToken() -> AccessToken +``` + + +Get the current access token for the authenticated user. + +This dependency provides access to the AccessToken for the current +authenticated request. Raises an error if no authentication is present. + +**Returns:** +- A dependency that resolves to the active AccessToken + +**Raises:** +- `RuntimeError`: If no authenticated user (use get_access_token() for optional) + + +### `TokenClaim` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/dependencies.py#L1346" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +TokenClaim(name: str) -> str +``` + + +Get a specific claim from the access token. + +This dependency extracts a single claim value from the current access token. +It's useful for getting user identifiers, roles, or other token claims +without needing the full token object. + +**Args:** +- `name`: The name of the claim to extract (e.g., "oid", "sub", "email") + +**Returns:** +- A dependency that resolves to the claim value as a string + +**Raises:** +- `RuntimeError`: If no access token is available or claim is missing + + +## Classes + +### `FastMCPRequestContext` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/dependencies.py#L55" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + + +FastMCP-owned wrapper around the SDK's per-request context. + +The SDK v2 runner hands each handler a fresh ``ServerRequestContext`` as an +argument rather than exposing it through a ContextVar. FastMCP owns this +ContextVar (``fastmcp_request_ctx``) and each request adapter binds a +``FastMCPRequestContext`` at the top of the handler (and the initialize +middleware binds it too). + +A wrapper rather than the raw context because the SDK's +``ServerRequestContext.meta`` is a bare ``RequestParamsMeta`` TypedDict that +only carries ``progress_token`` — it does not carry ``_meta.fastmcp`` or the +distributed-trace parent. Those live in the raw params dict under ``_meta``, +which this wrapper lifts once so downstream consumers have a stable surface. + + +### `ProgressLike` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/dependencies.py#L1099" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + + +Protocol for progress tracking interface. + +Defines the common interface between InMemoryProgress (server context) +and Docket's Progress (worker context). + + +**Methods:** + +#### `current` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/dependencies.py#L1107" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +current(self) -> int | None +``` + +Current progress value. + + +#### `total` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/dependencies.py#L1112" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +total(self) -> int +``` + +Total/target progress value. + + +#### `message` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/dependencies.py#L1117" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +message(self) -> str | None +``` + +Current progress message. + + +#### `set_total` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/dependencies.py#L1121" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +set_total(self, total: int) -> None +``` + +Set the total/target value for progress tracking. + + +#### `increment` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/dependencies.py#L1125" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +increment(self, amount: int = 1) -> None +``` + +Atomically increment the current progress value. + + +#### `set_message` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/dependencies.py#L1129" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +set_message(self, message: str | None) -> None +``` + +Update the progress status message. + + +### `InMemoryProgress` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/dependencies.py#L1134" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + + +In-memory progress tracker for immediate tool execution. + +Provides the same interface as Docket's Progress but stores state in memory +instead of Redis. Useful for testing and immediate execution where +progress doesn't need to be observable across processes. + + +**Methods:** + +#### `current` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/dependencies.py#L1159" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +current(self) -> int | None +``` + +#### `total` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/dependencies.py#L1163" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +total(self) -> int +``` + +#### `message` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/dependencies.py#L1167" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +message(self) -> str | None +``` + +#### `set_total` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/dependencies.py#L1170" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +set_total(self, total: int) -> None +``` + +Set the total/target value for progress tracking. + + +#### `increment` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/dependencies.py#L1176" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +increment(self, amount: int = 1) -> None +``` + +Atomically increment the current progress value. + + +#### `set_message` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/dependencies.py#L1185" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +set_message(self, message: str | None) -> None +``` + +Update the progress status message. + + +### `Progress` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/dependencies.py#L1190" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + + +Progress dependency that works in both server and worker contexts. + +In a Docket worker, delegates to the execution's Redis-backed progress +(observable across processes). Otherwise, uses in-memory tracking. + +The shared default instance acts as a stateless factory — ``__aenter__`` +creates a fresh ``Progress`` per invocation so concurrent tasks never +share mutable state. + + +**Methods:** + +#### `current` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/dependencies.py#L1231" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +current(self) -> int | None +``` + +Current progress value. + + +#### `total` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/dependencies.py#L1237" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +total(self) -> int +``` + +Total/target progress value. + + +#### `message` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/dependencies.py#L1243" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +message(self) -> str | None +``` + +Current progress message. + + +#### `set_total` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/dependencies.py#L1248" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +set_total(self, total: int) -> None +``` + +Set the total/target value for progress tracking. + + +#### `increment` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/dependencies.py#L1253" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +increment(self, amount: int = 1) -> None +``` + +Atomically increment the current progress value. + + +#### `set_message` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/dependencies.py#L1258" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +set_message(self, message: str | None) -> None +``` + +Update the progress status message. + diff --git a/docs/python-sdk/fastmcp-server-elicitation.mdx b/docs/python-sdk/fastmcp-server-elicitation.mdx new file mode 100644 index 000000000..824ab59e9 --- /dev/null +++ b/docs/python-sdk/fastmcp-server-elicitation.mdx @@ -0,0 +1,152 @@ +--- +title: elicitation +sidebarTitle: elicitation +--- + +# `fastmcp.server.elicitation` + +## Functions + +### `parse_elicit_response_type` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/elicitation.py#L143" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +parse_elicit_response_type(response_type: Any, response_title: str | None = None, response_description: str | None = None) -> ElicitConfig +``` + + +Parse response_type into schema and handling configuration. + +A response type is required; ``None`` raises ``TypeError``. Supports +multiple syntaxes: +- dict: `{"low": {"title": "..."}}` -> single-select titled enum +- list patterns: + - `[["a", "b"]]` -> multi-select untitled + - `[{"low": {...}}]` -> multi-select titled + - `["a", "b"]` -> single-select untitled +- `list\[X]` type annotation: multi-select with type +- Scalar types (bool, int, float, str, Literal, Enum): single value +- Other types (dataclass, BaseModel): use directly + +The ``response_title`` and ``response_description`` arguments customize the +label and description of the wrapped ``value`` property for the scalar/dict/list +shorthand forms. They are only valid when FastMCP is wrapping the response +type; passing them with a full BaseModel/dataclass raises ``TypeError``, +because in those cases the user already controls field metadata via +``Field(title=..., description=...)``. + + +### `handle_elicit_accept` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/elicitation.py#L310" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +handle_elicit_accept(config: ElicitConfig, content: Any) -> AcceptedElicitation[Any] +``` + + +Handle an accepted elicitation response. + +**Args:** +- `config`: The elicitation configuration from parse_elicit_response_type +- `content`: The response content from the client + +**Returns:** +- AcceptedElicitation with the extracted/validated data + + +### `get_elicitation_schema` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/elicitation.py#L369" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +get_elicitation_schema(response_type: type[T]) -> dict[str, Any] +``` + + +Get the schema for an elicitation response. + +**Args:** +- `response_type`: The type of the response + + +### `validate_elicitation_json_schema` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/elicitation.py#L395" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +validate_elicitation_json_schema(schema: dict[str, Any]) -> None +``` + + +Validate that a JSON schema follows MCP elicitation requirements. + +This ensures the schema is compatible with MCP elicitation requirements: +- Must be an object schema +- Must only contain primitive field types (string, number, integer, boolean) +- Must be flat (no nested objects or arrays of objects) +- Allows const fields (for Literal types) and enum fields (for Enum types) +- Only primitive types and their nullable variants are allowed + +**Args:** +- `schema`: The JSON schema to validate + +**Raises:** +- `TypeError`: If the schema doesn't meet MCP elicitation requirements + + +## Classes + +### `ElicitationJsonSchema` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/elicitation.py#L36" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + + +Custom JSON schema generator for MCP elicitation that always inlines enums. + +MCP elicitation requires inline enum schemas without $ref/$defs references. +This generator ensures enums are always generated inline for compatibility. +Optionally adds enumNames for better UI display when available. + + +**Methods:** + +#### `generate_inner` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/elicitation.py#L44" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +generate_inner(self, schema: core_schema.CoreSchema) -> JsonSchemaValue +``` + +Override to prevent ref generation for enums and handle list schemas. + + +#### `list_schema` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/elicitation.py#L57" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +list_schema(self, schema: core_schema.ListSchema) -> JsonSchemaValue +``` + +Generate schema for list types, detecting enum items for multi-select. + + +#### `enum_schema` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/elicitation.py#L94" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +enum_schema(self, schema: core_schema.EnumSchema) -> JsonSchemaValue +``` + +Generate inline enum schema. + +Always generates enum pattern: `{"enum": [value, ...]}` +Titled enums are handled separately via dict-based syntax in ctx.elicit(). + + +### `AcceptedElicitation` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/elicitation.py#L105" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + + +Result when user accepts the elicitation. + + +### `ScalarElicitationType` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/elicitation.py#L113" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +### `ElicitConfig` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/elicitation.py#L118" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + + +Configuration for an elicitation request. + +**Attributes:** +- `schema`: The JSON schema to send to the client +- `response_type`: The type to validate responses with (None for raw schemas) +- `is_raw`: True if schema was built directly (extract "value" from response) + diff --git a/docs/python-sdk/fastmcp-server-event_store.mdx b/docs/python-sdk/fastmcp-server-event_store.mdx new file mode 100644 index 000000000..08d266ea5 --- /dev/null +++ b/docs/python-sdk/fastmcp-server-event_store.mdx @@ -0,0 +1,78 @@ +--- +title: event_store +sidebarTitle: event_store +--- + +# `fastmcp.server.event_store` + + +EventStore implementation backed by AsyncKeyValue. + +This module provides an EventStore implementation that enables SSE polling/resumability +for Streamable HTTP transports. Events are stored using the key_value package's +AsyncKeyValue protocol, allowing users to configure any compatible backend +(in-memory, Redis, etc.) following the same pattern as ResponseCachingMiddleware. + + +## Classes + +### `EventEntry` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/event_store.py#L34" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + + +Stored event entry. + + +### `StreamEventList` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/event_store.py#L42" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + + +List of event IDs for a stream. + + +### `EventStore` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/event_store.py#L48" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + + +EventStore implementation backed by AsyncKeyValue. + +Enables SSE polling/resumability by storing events that can be replayed +when clients reconnect. Works with any AsyncKeyValue backend (memory, Redis, etc.) +following the same pattern as ResponseCachingMiddleware and OAuthProxy. + +**Args:** +- `storage`: AsyncKeyValue backend. Defaults to MemoryStore. +- `max_events_per_stream`: Maximum events to retain per stream. Default 100. +- `ttl`: Event TTL in seconds. Default 3600 (1 hour). Set to None for no expiration. + + +**Methods:** + +#### `store_event` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/event_store.py#L102" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +store_event(self, stream_id: StreamId, message: JSONRPCMessage | None) -> EventId +``` + +Store an event and return its ID. + +**Args:** +- `stream_id`: ID of the stream the event belongs to +- `message`: The JSON-RPC message to store, or None for priming events + +**Returns:** +- The generated event ID for the stored event + + +#### `replay_events_after` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/event_store.py#L143" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +replay_events_after(self, last_event_id: EventId, send_callback: EventCallback) -> StreamId | None +``` + +Replay events that occurred after the specified event ID. + +**Args:** +- `last_event_id`: The ID of the last event the client received +- `send_callback`: A callback function to send events to the client + +**Returns:** +- The stream ID of the replayed events, or None if the event ID was not found + diff --git a/docs/python-sdk/fastmcp-server-extensions.mdx b/docs/python-sdk/fastmcp-server-extensions.mdx new file mode 100644 index 000000000..3c8c2f62f --- /dev/null +++ b/docs/python-sdk/fastmcp-server-extensions.mdx @@ -0,0 +1,194 @@ +--- +title: extensions +sidebarTitle: extensions +--- + +# `fastmcp.server.extensions` + + +FastMCP-native server extension API (SEP-2133). + +An MCP extension is an opt-in, capability-negotiated bundle of protocol +behaviour identified by a reverse-DNS string (e.g. `io.modelcontextprotocol/tasks`). +Unlike the SDK's `mcp.server.extension.Extension`, a FastMCP `ServerExtension` +is bound to its `FastMCP` instance at registration, so its request handlers and +its `tools/call` interceptor can reach the component registry, `Context`, and +auth scope that the SDK's model withholds. + +An extension contributes any subset of four things: + +- **A negotiated capability.** `settings()` is spliced into + `ServerCapabilities.extensions[identifier]` (see `LowLevelServer.get_capabilities`). +- **New request methods.** `methods()` returns `MethodBinding`s, each wired onto + the low-level server via `add_request_handler` when the extension is registered. +- **A `tools/call` interceptor.** `intercept_tool_call()` is the last gate before + a tool body runs — it composes *after* the FastMCP middleware chain and *before* + component execution, so it can observe, short-circuit, or pass a call through. +- **A lifespan.** `lifespan()` is entered with the server's lifespan and exited on + shutdown — the hook the SDK's `Extension` lacks, needed to start backends/workers. + +The base class follows the SDK's httpx-style shape: every contribution method has +a default, so a subclass overrides only what it needs. + + +## Functions + +### `read_client_extension_settings` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/extensions.py#L235" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +read_client_extension_settings(ctx: ServerRequestContext[Any, Any], identifier: str) -> dict[str, Any] | None +``` + + +Read a client's per-request extension opt-in from the request `_meta`. + +SEP-2133 extensions negotiate per request: the client repeats its extension +capabilities in each request's `_meta` under +`io.modelcontextprotocol/clientCapabilities` → `extensions` → `identifier`. +Returns the declared settings dict (possibly empty) when the extension was +opted in for this request, or `None` when it was not. + + +### `build_method_handler` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/extensions.py#L249" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +build_method_handler(binding: MethodBinding) -> ExtensionRequestHandler +``` + + +Wrap a `MethodBinding` into a low-level request handler. + +The adapter enforces `protocol_versions` gating (rejecting other versions as +`METHOD_NOT_FOUND`, since `add_request_handler` registers unconditionally) +and binds the FastMCP request context so the handler can use `get_context()`, +auth, and other request-scoped dependencies. + + +### `wrap_tool_call_interceptor` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/extensions.py#L278" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +wrap_tool_call_interceptor(extension: ServerExtension, call_next: Callable[[Any], Awaitable[Any]]) -> Callable[[Any], Awaitable[Any]] +``` + + +Fold one extension's `intercept_tool_call` around a middleware `call_next`. + +The returned wrapper is a FastMCP `CallNext`: it hands the extension the +validated `tools/call` params, the FastMCP `Context`, and a zero-arg +continuation that runs the rest of the chain and, finally, the tool body. + + +## Classes + +### `MethodBinding` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/extensions.py#L77" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + + +A new request method an extension serves, e.g. `tasks/get`. + +`params_type` validates incoming params before `handler` runs; it should +subclass `RequestParams` so `_meta` parses uniformly. `protocol_versions`, +when set, restricts the method to those wire versions — a request at any +other version is rejected as `METHOD_NOT_FOUND`, mirroring the spec's +`(method, version)` boundary. `None` (the default) admits every version. + +Extension methods are additive: `method` must not name a spec-defined +request method (`tools/call`, `completion/complete`, ...). Binding one would +silently shadow the server's own handler. Both constraints are enforced at +construction. + + +### `ServerExtension` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/extensions.py#L111" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + + +Base class for an opt-in FastMCP server extension (SEP-2133). + +Subclass, set `identifier`, and override the contribution methods that +apply. Every method has a default, so a minimal extension overrides only +`identifier` and one contribution. `identifier` is validated at +subclass-definition time when set as a class attribute, and again at +registration (which covers per-instance identifiers assigned in `__init__`). + +Register an instance with `FastMCP.add_extension(...)`, which binds the +extension to the server so `self.server`, `intercept_tool_call`, and method +handlers can reach FastMCP-level constructs. + + +**Methods:** + +#### `server` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/extensions.py#L149" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +server(self) -> FastMCP +``` + +The FastMCP server this extension is registered on. + +Handlers, interceptors, and lifespan code reach the component registry, +`Context`, and auth scope through here. Raises if the extension has not +been registered with `FastMCP.add_extension()`. + + +#### `settings` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/extensions.py#L165" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +settings(self) -> dict[str, Any] +``` + +Per-extension settings advertised at `capabilities.extensions[identifier]`. + +An empty dict (the default) advertises the extension with no settings. + + +#### `methods` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/extensions.py#L172" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +methods(self) -> Sequence[MethodBinding] +``` + +New request methods this extension serves (additive). + + +#### `lifespan` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/extensions.py#L176" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +lifespan(self) -> AbstractAsyncContextManager[None] +``` + +A context manager entered with the server's lifespan, exited on shutdown. + +Default: a no-op. Override to start and stop resources an extension owns +(a task-queue backend and worker, say). Entered once per runtime tree, at +the root — a mounted child defers to the root, as the shared Docket does. + + +#### `intercept_tool_call` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/extensions.py#L185" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +intercept_tool_call(self, params: CallToolRequestParams, context: Context, call_next: ToolCallContinuation) -> ToolCallOutcome +``` + +Wrap `tools/call`. Default: pass through unchanged. + +Runs after the FastMCP middleware chain and before the tool body, so it +is the last gate before execution. Override to observe the call, to +short-circuit (return a result without awaiting `call_next`), or to pass +it through (`return await call_next()`). `params` is the validated +`tools/call` params; `context` is the FastMCP `Context`, from which the +tool being called (`context.fastmcp.get_tool(params.name)`), auth scope, +and the server are reachable. Multiple extensions nest with the +first-registered outermost. + + +#### `client_settings` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/extensions.py#L204" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +client_settings(self, ctx: ServerRequestContext[Any, Any]) -> dict[str, Any] | None +``` + +This extension's per-request opt-in settings declared by the client. + +Reads the request's `_meta` client-capabilities block. Returns the +declared settings dict (possibly empty) when the client opted this +extension in for the request, or `None` when it did not. Convenience for +`read_client_extension_settings(ctx, self.identifier)`. + diff --git a/docs/python-sdk/fastmcp-server-http.mdx b/docs/python-sdk/fastmcp-server-http.mdx new file mode 100644 index 000000000..46db6c15a --- /dev/null +++ b/docs/python-sdk/fastmcp-server-http.mdx @@ -0,0 +1,144 @@ +--- +title: http +sidebarTitle: http +--- + +# `fastmcp.server.http` + +## Functions + +### `set_http_request` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/http.py#L355" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +set_http_request(request: Request) -> Generator[Request, None, None] +``` + +### `create_base_app` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/http.py#L388" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +create_base_app(routes: list[BaseRoute], middleware: list[Middleware], debug: bool = False, lifespan: Callable | None = None) -> StarletteWithLifespan +``` + + +Create a base Starlette app with common middleware and routes. + +**Args:** +- `routes`: List of routes to include in the app +- `middleware`: List of middleware to include in the app +- `debug`: Whether to enable debug mode +- `lifespan`: Optional lifespan manager for the app + +**Returns:** +- A Starlette application + + +### `create_sse_app` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/http.py#L416" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +create_sse_app(server: FastMCP[LifespanResultT], message_path: str, sse_path: str, auth: AuthProvider | None = None, debug: bool = False, routes: list[BaseRoute] | None = None, middleware: list[Middleware] | None = None) -> StarletteWithLifespan +``` + + +Return an instance of the SSE server app. + +**Args:** +- `server`: The FastMCP server instance +- `message_path`: Path for SSE messages +- `sse_path`: Path for SSE connections +- `auth`: Optional authentication provider (AuthProvider) +- `debug`: Whether to enable debug mode +- `routes`: Optional list of custom routes +- `middleware`: Optional list of middleware + +Returns: + A Starlette application with RequestContextMiddleware + + +### `create_streamable_http_app` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/http.py#L545" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +create_streamable_http_app(server: FastMCP[LifespanResultT], streamable_http_path: str, event_store: EventStore | None = None, retry_interval: int | None = None, auth: AuthProvider | None = None, json_response: bool = False, stateless_http: bool = False, debug: bool = False, routes: list[BaseRoute] | None = None, middleware: list[Middleware] | None = None, host_origin_protection: HostOriginProtection = False, allowed_hosts: Sequence[str] | None = None, allowed_origins: Sequence[str] | None = None, session_idle_timeout: float | None = None) -> StarletteWithLifespan +``` + + +Return an instance of the StreamableHTTP server app. + +**Args:** +- `server`: The FastMCP server instance +- `streamable_http_path`: Path for StreamableHTTP connections +- `event_store`: Optional event store for SSE polling/resumability +- `retry_interval`: Optional retry interval in milliseconds for SSE polling. +Controls how quickly clients should reconnect after server-initiated +disconnections. Requires event_store to be set. Defaults to SDK default. +- `auth`: Optional authentication provider (AuthProvider) +- `json_response`: Whether to use JSON response format +- `stateless_http`: Whether to use stateless mode (new transport per request) +- `debug`: Whether to enable debug mode +- `routes`: Optional list of custom routes +- `middleware`: Optional list of middleware +- `host_origin_protection`: Whether to validate Host and Origin headers +before requests reach the MCP endpoint. Defaults to False for +compatibility. "auto" protects localhost-bound servers and explicit +host/origin allowlists. +- `allowed_hosts`: Additional hostnames that may appear in the Host header. +- `allowed_origins`: Additional browser origins trusted by the request guard. +Configure CORS separately when browser JavaScript must read +cross-origin responses. +- `session_idle_timeout`: Maximum time in seconds a session may remain idle +before it is terminated. The deadline is pushed forward on every +request. When None, sessions never expire from inactivity. Not +supported in stateless mode. + +**Returns:** +- A Starlette application with StreamableHTTP support + + +## Classes + +### `FastMCPStreamableHTTPSessionManager` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/http.py#L43" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + + +Session manager that scopes resumability storage per transport session. + + +**Methods:** + +#### `event_store` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/http.py#L68" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +event_store(self) -> EventStore | None +``` + +#### `event_store` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/http.py#L76" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +event_store(self, event_store: EventStore | None) -> None +``` + +### `StreamableHTTPASGIApp` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/http.py#L80" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + + +ASGI application wrapper for Streamable HTTP server transport. + + +### `HostOriginGuardMiddleware` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/http.py#L227" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + + +Validate Host and Origin headers before requests reach MCP sessions. + + +### `StarletteWithLifespan` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/http.py#L348" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +**Methods:** + +#### `lifespan` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/http.py#L350" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +lifespan(self) -> Lifespan[Starlette] +``` + +### `RequestContextMiddleware` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/http.py#L363" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + + +Middleware that stores each request in a ContextVar and sets transport type. + diff --git a/docs/python-sdk/fastmcp-server-lifespan.mdx b/docs/python-sdk/fastmcp-server-lifespan.mdx new file mode 100644 index 000000000..091836304 --- /dev/null +++ b/docs/python-sdk/fastmcp-server-lifespan.mdx @@ -0,0 +1,101 @@ +--- +title: lifespan +sidebarTitle: lifespan +--- + +# `fastmcp.server.lifespan` + + +Composable lifespans for FastMCP servers. + +This module provides a `@lifespan` decorator for creating composable server lifespans +that can be combined using the `|` operator. + +Example: + ```python + from fastmcp import FastMCP + from fastmcp.server.lifespan import lifespan + + @lifespan + async def db_lifespan(server): + conn = await connect_db() + yield {"db": conn} + await conn.close() + + @lifespan + async def cache_lifespan(server): + cache = await connect_cache() + yield {"cache": cache} + await cache.close() + + mcp = FastMCP("server", lifespan=db_lifespan | cache_lifespan) + ``` + +To compose with existing `@asynccontextmanager` lifespans, wrap them explicitly: + + ```python + from contextlib import asynccontextmanager + from fastmcp.server.lifespan import lifespan, ContextManagerLifespan + + @asynccontextmanager + async def legacy_lifespan(server): + yield {"legacy": True} + + @lifespan + async def new_lifespan(server): + yield {"new": True} + + # Wrap the legacy lifespan explicitly + combined = ContextManagerLifespan(legacy_lifespan) | new_lifespan + ``` + + +## Functions + +### `lifespan` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/lifespan.py#L172" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +lifespan(fn: LifespanFn) -> Lifespan +``` + + +Decorator to create a composable lifespan. + +Use this decorator on an async generator function to make it composable +with other lifespans using the `|` operator. + +**Args:** +- `fn`: An async generator function that takes a FastMCP server and yields +a dict for the lifespan context. + +**Returns:** +- A composable Lifespan wrapper. + + +## Classes + +### `Lifespan` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/lifespan.py#L61" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + + +Composable lifespan wrapper. + +Wraps an async generator function and enables composition via the `|` operator. +The wrapped function should yield a dict that becomes part of the lifespan context. + + +### `ContextManagerLifespan` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/lifespan.py#L110" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + + +Lifespan wrapper for already-wrapped context manager functions. + +Use this for functions already decorated with @asynccontextmanager. + + +### `ComposedLifespan` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/lifespan.py#L137" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + + +Two lifespans composed together. + +Enters the left lifespan first, then the right. Exits in reverse order. +Results are shallow-merged into a single dict. + diff --git a/docs/python-sdk/fastmcp-server-low_level.mdx b/docs/python-sdk/fastmcp-server-low_level.mdx new file mode 100644 index 000000000..f515849e4 --- /dev/null +++ b/docs/python-sdk/fastmcp-server-low_level.mdx @@ -0,0 +1,106 @@ +--- +title: low_level +sidebarTitle: low_level +--- + +# `fastmcp.server.low_level` + +## Functions + +### `client_supports_extension` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/low_level.py#L111" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +client_supports_extension(session: ServerSession, extension_id: str) -> bool +``` + + +Check whether the connected client supports a given MCP extension. + +Inspects the ``extensions`` capability on ``ClientCapabilities`` sent by the +client during initialization. In v2 the client's initialize params are +reachable via ``session.client_params``. + +SDK v2 declares ``extensions`` as a real field on ``ClientCapabilities``, so +a client sending ``ClientCapabilities(extensions={...})`` populates the field +directly. We read that field first and fall back to ``model_extra`` only for +legacy-serialized clients that carried ``extensions`` as an extra key. + + +## Classes + +### `FastMCPServerMiddleware` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/low_level.py#L140" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + + +Root dispatch for the FastMCP middleware chain, in the SDK's middleware layer. + +v2 no longer lets FastMCP subclass ``ServerSession`` (the runner constructs +it per request), so the old ``MiddlewareServerSession._received_request`` +override is replaced by a ``ServerMiddleware`` — an ordinary entry in the +SDK's own middleware list. Sitting at the root of dispatch, this +is the single entry point through which *every* inbound message flows — +requests, notifications, cancellations, ``initialize``, and even malformed or +unroutable messages the SDK can still hand us. It binds the FastMCP +request-context ContextVar and re-applies the app-scoped ``SharedContext`` for +the whole chain, then runs the FastMCP ``Middleware`` chain so +``on_message`` / ``on_request`` / ``on_notification`` observe the message. + +Dispatch shapes: + +- Negotiation runs the *whole* FastMCP chain here: ``initialize`` dispatches + through ``on_initialize`` and ``server/discover`` through ``on_discover``. + Neither has an interior FastMCP handler adapter, and the SDK serializes both + results before returning through its middleware seam, so this root adapter + restores core results to typed models before FastMCP middleware observes them. +- The component methods (``tools/call``, ``tools/list``, ``resources/read``, + ...) still run their FastMCP chain *interior*, in the handler adapter, where + ``on_call_tool`` receives the typed component result and a tool exception + propagates through ``on_message``/``on_request`` exactly where the built-in + error/logging/timing middleware expect it. The root dispatch does not re-run the + chain for these — it only steps in when such a request fails *before* the + interior runs (malformed params, routing), so ``on_message`` still observes + the failure. +- Every other message — all notifications (including ``notifications/cancelled`` + and ``notifications/initialized``), ``ping``, ``logging/setLevel``, and any + unroutable/non-component request — has no interior FastMCP dispatch, so the + root dispatch runs the ``"outer"`` pass (``on_message`` plus + ``on_request``/``on_notification``) here, wrapping the real SDK dispatch. + This closes the long-standing gap where these messages were invisible to + FastMCP middleware. + + +### `LowLevelServer` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/low_level.py#L455" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +**Methods:** + +#### `fastmcp` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/low_level.py#L507" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +fastmcp(self) -> FastMCP +``` + +Get the FastMCP instance. + + +#### `create_initialization_options` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/low_level.py#L514" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +create_initialization_options(self, notification_options: NotificationOptions | None = None, experimental_capabilities: dict[str, dict[str, Any]] | None = None, extensions: dict[str, dict[str, Any]] | None = None) -> InitializationOptions +``` + +#### `get_capabilities` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/low_level.py#L529" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +get_capabilities(self, notification_options: NotificationOptions | None = None, experimental_capabilities: dict[str, dict[str, Any]] | None = None, extensions: dict[str, dict[str, Any]] | None = None) -> mcp_types.ServerCapabilities +``` + +Override to advertise registered extensions and the MCP Apps UI extension. + +``ServerCapabilities.extensions`` is a real declared field in v2, so we +update it directly. The +`FastMCP(experimental_capabilities=...)` merge also lives here rather +than in `create_initialization_options`: the modern `server/discover` +handler calls this directly, without going through +`create_initialization_options` at all, so merging there only reached +the handshake-era `initialize` response and silently dropped +constructor-configured experimental capabilities from `discover`. + diff --git a/docs/python-sdk/fastmcp-server-mixins.mdx b/docs/python-sdk/fastmcp-server-mixins.mdx new file mode 100644 index 000000000..9734da93c --- /dev/null +++ b/docs/python-sdk/fastmcp-server-mixins.mdx @@ -0,0 +1,9 @@ +--- +title: mixins +sidebarTitle: mixins +--- + +# `fastmcp.server.mixins` + + +Server mixins for FastMCP. diff --git a/docs/python-sdk/fastmcp-server-providers.mdx b/docs/python-sdk/fastmcp-server-providers.mdx new file mode 100644 index 000000000..c227ee1a0 --- /dev/null +++ b/docs/python-sdk/fastmcp-server-providers.mdx @@ -0,0 +1,34 @@ +--- +title: providers +sidebarTitle: providers +--- + +# `fastmcp.server.providers` + + +Providers for dynamic MCP components. + +This module provides the `Provider` abstraction for providing tools, +resources, and prompts dynamically at runtime. + +Example: + ```python + from fastmcp import FastMCP + from fastmcp.server.providers import Provider + from fastmcp.tools import Tool + + class DatabaseProvider(Provider): + def __init__(self, db_url: str): + self.db = Database(db_url) + + async def _list_tools(self) -> list[Tool]: + rows = await self.db.fetch("SELECT * FROM tools") + return [self._make_tool(row) for row in rows] + + async def _get_tool(self, name: str) -> Tool | None: + row = await self.db.fetchone("SELECT * FROM tools WHERE name = ?", name) + return self._make_tool(row) if row else None + + mcp = FastMCP("Server", providers=[DatabaseProvider(db_url)]) + ``` + diff --git a/docs/python-sdk/fastmcp-server-server.mdx b/docs/python-sdk/fastmcp-server-server.mdx new file mode 100644 index 000000000..12524e5b8 --- /dev/null +++ b/docs/python-sdk/fastmcp-server-server.mdx @@ -0,0 +1,891 @@ +--- +title: server +sidebarTitle: server +--- + +# `fastmcp.server.server` + + +FastMCP - A more ergonomic interface for MCP servers. + +## Functions + +### `default_lifespan` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/server.py#L237" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +default_lifespan(server: FastMCP[LifespanResultT]) -> AsyncIterator[Any] +``` + + +Default lifespan context manager that does nothing. + +**Args:** +- `server`: The server instance this lifespan is managing + +**Returns:** +- An empty dictionary as the lifespan result. + + +### `create_proxy` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/server.py#L2501" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +create_proxy(target: Client[ClientTransportT] | ClientTransport | FastMCP[Any] | SDKServer | AnyUrl | Path | MCPConfig | dict[str, Any] | str, **settings: Any) -> FastMCPProxy +``` + + +Create a FastMCP proxy server for the given target. + +This is the recommended way to create a proxy server. For lower-level control, +use `FastMCPProxy` or `ProxyProvider` directly from `fastmcp.server.providers.proxy`. + +**Args:** +- `target`: The backend to proxy to. Can be\: +- A Client instance (connected or disconnected) +- A ClientTransport +- A FastMCP server instance +- A URL string or AnyUrl +- A Path to a server script +- An MCPConfig or dict +- `mode`: Protocol-era negotiation for auto-created proxy clients (a +non-Client target). By default (``None``) the backend MIRRORS the +front connection's negotiated era per request, so the whole chain +speaks one era end-to-end\: a modern front reaches a modern backend +(a guard tool's `InputRequiredResult` (SEP-2322) round-trips) and a +handshake front reaches a handshake backend (server-initiated +sampling / elicitation / roots push-forwarding works). Pass an +explicit mode (e.g. ``"auto"`` or a version string) to pin the +backend era regardless of the front; this overrides mirroring and is +appropriate when the backend only speaks one era. Ignored when +`target` is already a `Client` (which carries its own mode). +- `**settings`: Additional settings passed to FastMCPProxy (name, etc.) + +**Returns:** +- A FastMCPProxy server that proxies to the target. + + +## Classes + +### `StateValue` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/server.py#L272" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + + +Wrapper for stored context state values. + + +### `FastMCP` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/server.py#L278" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +**Methods:** + +#### `name` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/server.py#L507" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +name(self) -> str +``` + +#### `instructions` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/server.py#L511" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +instructions(self) -> str | None +``` + +#### `instructions` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/server.py#L515" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +instructions(self, value: str | None) -> None +``` + +#### `version` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/server.py#L519" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +version(self) -> str | None +``` + +#### `website_url` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/server.py#L523" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +website_url(self) -> str | None +``` + +#### `icons` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/server.py#L527" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +icons(self) -> list[mcp_types.Icon] +``` + +#### `local_provider` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/server.py#L534" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +local_provider(self) -> LocalProvider +``` + +The server's local provider, which stores directly-registered components. + +Use this to remove components: + + mcp.local_provider.remove_tool("my_tool") + mcp.local_provider.remove_resource("data://info") + mcp.local_provider.remove_prompt("my_prompt") + + +#### `add_middleware` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/server.py#L598" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +add_middleware(self, middleware: Middleware) -> None +``` + +#### `add_extension` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/server.py#L601" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +add_extension(self, extension: ServerExtension) -> None +``` + +Register a server extension (SEP-2133). + +An extension contributes a negotiated capability, additive request +methods, a `tools/call` interceptor, and an optional lifespan — each +with access to FastMCP-level constructs (the component registry, +`Context`, auth scope). Its capability is advertised only while it is +registered. + +The extension is bound to this server (so its handlers and interceptor +can reach it), its method bindings are wired onto the low-level server, +and it is recorded for capability advertisement, interception, and +lifespan entry. Registering two extensions with the same identifier is +an error, as is registering after the server's lifespan has started — +the extension's lifespan could no longer run, leaving it silently +half-active. + +Extensions are served by the server they are registered on. A mounted +child's extensions do not propagate to the root: the root serves the +wire, so only root-registered extensions advertise capabilities and +answer methods (matching the lifespan, which also defers to the root). +Register extensions on the server you run. + + +#### `add_provider` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/server.py#L673" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +add_provider(self, provider: Provider) -> None +``` + +Add a provider for dynamic tools, resources, and prompts. + +Providers are queried in registration order. The first provider to return +a non-None result wins. Static components (registered via decorators) +always take precedence over providers. + +**Args:** +- `provider`: A Provider instance that will provide components dynamically. +- `namespace`: Optional namespace prefix. When set\: +- Tools become "namespace_toolname" +- Resources become "protocol\://namespace/path" +- Prompts become "namespace_promptname" + + +#### `get_tasks` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/server.py#L785" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +get_tasks(self) -> Sequence[FastMCPComponent] +``` + +Get task-eligible components with all transforms applied. + +Overrides AggregateProvider.get_tasks() to apply server-level transforms +after aggregation. AggregateProvider handles provider-level namespacing. + + +#### `add_transform` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/server.py#L814" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +add_transform(self, transform: Transform) -> None +``` + +Add a server-level transform. + +Server-level transforms are applied after all providers are aggregated. +They transform tools, resources, and prompts from ALL providers. + +**Args:** +- `transform`: The transform to add. + + +#### `list_tools` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/server.py#L834" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +list_tools(self) -> Sequence[Tool] +``` + +List all enabled tools from providers. + +Overrides Provider.list_tools() to add enabled filtering, auth filtering, +and middleware execution. Returns all versions (no deduplication). +Protocol handlers deduplicate for MCP wire format. + + +#### `get_tool` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/server.py#L917" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +get_tool(self, name: str, version: VersionSpec | None = None) -> Tool | None +``` + +Get a tool by name, filtering disabled tools. + +Overrides Provider.get_tool() to filter disabled tools after all +transforms (including session-level) have been applied. This ensures +session transforms can override provider-level disables. + +When the highest version is disabled and no explicit version was +requested, falls back to the next-highest enabled version. + +**Args:** +- `name`: The tool name. +- `version`: Version filter (None returns highest version). + +**Returns:** +- The tool if found and enabled, None otherwise. + + +#### `list_resources` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/server.py#L971" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +list_resources(self) -> Sequence[Resource] +``` + +List all enabled resources from providers. + +Overrides Provider.list_resources() to add visibility filtering, auth filtering, +and middleware execution. Returns all versions (no deduplication). +Protocol handlers deduplicate for MCP wire format. + + +#### `get_resource` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/server.py#L1056" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +get_resource(self, uri: str, version: VersionSpec | None = None) -> Resource | None +``` + +Get a resource by URI, filtering disabled resources. + +Overrides Provider.get_resource() to add visibility filtering after all +transforms (including session-level) have been applied. + +When the highest version is disabled and no explicit version was +requested, falls back to the next-highest enabled version. + +**Args:** +- `uri`: The resource URI. +- `version`: Version filter (None returns highest version). + +**Returns:** +- The resource if found and enabled, None otherwise. + + +#### `list_resource_templates` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/server.py#L1106" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +list_resource_templates(self) -> Sequence[ResourceTemplate] +``` + +List all enabled resource templates from providers. + +Overrides Provider.list_resource_templates() to add visibility filtering, +auth filtering, and middleware execution. Returns all versions (no deduplication). +Protocol handlers deduplicate for MCP wire format. + + +#### `get_resource_template` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/server.py#L1188" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +get_resource_template(self, uri: str, version: VersionSpec | None = None) -> ResourceTemplate | None +``` + +Get a resource template by URI, filtering disabled templates. + +Overrides Provider.get_resource_template() to add visibility filtering after +all transforms (including session-level) have been applied. + +When the highest version is disabled and no explicit version was +requested, falls back to the next-highest enabled version. + +**Args:** +- `uri`: The template URI. +- `version`: Version filter (None returns highest version). + +**Returns:** +- The template if found and enabled, None otherwise. + + +#### `list_prompts` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/server.py#L1242" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +list_prompts(self) -> Sequence[Prompt] +``` + +List all enabled prompts from providers. + +Overrides Provider.list_prompts() to add visibility filtering, auth filtering, +and middleware execution. Returns all versions (no deduplication). +Protocol handlers deduplicate for MCP wire format. + + +#### `get_prompt` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/server.py#L1314" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +get_prompt(self, name: str, version: VersionSpec | None = None) -> Prompt | None +``` + +Get a prompt by name, filtering disabled prompts. + +Overrides Provider.get_prompt() to add visibility filtering after all +transforms (including session-level) have been applied. + +When the highest version is disabled and no explicit version was +requested, falls back to the next-highest enabled version. + +**Args:** +- `name`: The prompt name. +- `version`: Version filter (None returns highest version). + +**Returns:** +- The prompt if found and enabled, None otherwise. + + +#### `call_tool` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/server.py#L1364" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +call_tool(self, name: str, arguments: dict[str, Any] | None = None) -> ToolResult +``` + +Call a tool by name. + +This is the public API for executing tools. By default, middleware is applied. + +**Args:** +- `name`: The tool name +- `arguments`: Tool arguments (optional) +- `version`: Specific version to call. If None, calls highest version. +- `run_middleware`: If True (default), apply the middleware chain. +Set to False when called from middleware to avoid re-applying. + +**Returns:** +- ToolResult. + +A guard tool that requests client input (SEP-2322 multi-round-trip) +returns an ``InputRequiredToolResult`` (a ``ToolResult`` subclass); it +flows back through the middleware chain as an ordinary result and the +wire handler unwraps it into an ``InputRequiredResult`` on the response. + +**Raises:** +- `NotFoundError`: If tool not found or disabled +- `ToolError`: If tool execution fails +- `ValidationError`: If arguments fail validation + + +#### `read_resource` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/server.py#L1557" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +read_resource(self, uri: str) -> ResourceResult +``` + +Read a resource by URI. + +This is the public API for reading resources. By default, middleware is applied. +Checks concrete resources first, then templates. + +**Args:** +- `uri`: The resource URI +- `version`: Specific version to read. If None, reads highest version. +- `run_middleware`: If True (default), apply the middleware chain. +Set to False when called from middleware to avoid re-applying. + +**Returns:** +- ResourceResult. + +**Raises:** +- `NotFoundError`: If resource not found or disabled +- `ResourceError`: If resource read fails + + +#### `render_prompt` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/server.py#L1715" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +render_prompt(self, name: str, arguments: dict[str, Any] | None = None) -> PromptResult +``` + +Render a prompt by name. + +This is the public API for rendering prompts. By default, middleware is applied. +Use get_prompt() to retrieve the prompt definition without rendering. + +**Args:** +- `name`: The prompt name +- `arguments`: Prompt arguments (optional) +- `version`: Specific version to render. If None, renders highest version. +- `run_middleware`: If True (default), apply the middleware chain. +Set to False when called from middleware to avoid re-applying. + +**Returns:** +- PromptResult. + +**Raises:** +- `NotFoundError`: If prompt not found or disabled +- `PromptError`: If prompt rendering fails + + +#### `add_tool` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/server.py#L1795" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +add_tool(self, tool: Tool | Callable[..., Any]) -> Tool +``` + +Add a tool to the server. + +The tool function can optionally request a Context object by adding a parameter +with the Context type annotation. See the @tool decorator for examples. + +**Args:** +- `tool`: The Tool instance or @tool-decorated function to register + +**Returns:** +- The tool instance that was added to the server. + + +#### `tool` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/server.py#L1810" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +tool(self, name_or_fn: F) -> F +``` + +#### `tool` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/server.py#L1831" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +tool(self, name_or_fn: str | None = None) -> Callable[[F], F] +``` + +#### `tool` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/server.py#L1851" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +tool(self, name_or_fn: str | AnyFunction | None = None) -> Callable[[AnyFunction], FunctionTool] | FunctionTool | partial[Callable[[AnyFunction], FunctionTool] | FunctionTool] +``` + +Decorator to register a tool. + +Tools can optionally request a Context object by adding a parameter with the +Context type annotation. The context provides access to MCP capabilities like +logging, progress reporting, and resource access. + +This decorator supports multiple calling patterns: +- @server.tool (without parentheses) +- @server.tool (with empty parentheses) +- @server.tool("custom_name") (with name as first argument) +- @server.tool(name="custom_name") (with name as keyword argument) +- server.tool(function, name="custom_name") (direct function call) + +**Args:** +- `name_or_fn`: Either a function (when used as @tool), a string name, or None +- `name`: Optional name for the tool (keyword-only, alternative to name_or_fn) +- `description`: Optional description of what the tool does +- `tags`: Optional set of tags for categorizing the tool +- `output_schema`: Optional JSON schema for the tool's output +- `annotations`: Optional annotations about the tool's behavior +- `meta`: Optional meta information about the tool + +**Examples:** + +Register a tool with a custom name: +```python +@server.tool +def my_tool(x: int) -> str: + return str(x) + +# Register a tool with a custom name +@server.tool +def my_tool(x: int) -> str: + return str(x) + +@server.tool("custom_name") +def my_tool(x: int) -> str: + return str(x) + +@server.tool(name="custom_name") +def my_tool(x: int) -> str: + return str(x) + +# Direct function call +server.tool(my_function, name="custom_name") +``` + + +#### `add_resource` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/server.py#L1948" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +add_resource(self, resource: Resource | Callable[..., Any]) -> Resource | ResourceTemplate +``` + +Add a resource to the server. + +**Args:** +- `resource`: A Resource instance or @resource-decorated function to add + +**Returns:** +- The resource instance that was added to the server. + + +#### `add_template` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/server.py#L1961" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +add_template(self, template: ResourceTemplate) -> ResourceTemplate +``` + +Add a resource template to the server. + +**Args:** +- `template`: A ResourceTemplate instance to add + +**Returns:** +- The template instance that was added to the server. + + +#### `resource` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/server.py#L1972" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +resource(self, uri: str) -> Callable[[F], F] +``` + +Decorator to register a function as a resource. + +The function will be called when the resource is read to generate its content. +The function can return: +- str for text content +- bytes for binary content +- other types will be converted to JSON + +Resources can optionally request a Context object by adding a parameter with the +Context type annotation. The context provides access to MCP capabilities like +logging, progress reporting, and session information. + +If the URI contains parameters (e.g. "resource://{param}") or the function +has parameters, it will be registered as a template resource. + +**Args:** +- `uri`: URI for the resource (e.g. "resource\://my-resource" or "resource\://{param}") +- `name`: Optional name for the resource +- `description`: Optional description of the resource +- `mime_type`: Optional MIME type for the resource +- `tags`: Optional set of tags for categorizing the resource +- `annotations`: Optional annotations about the resource's behavior +- `meta`: Optional meta information about the resource + +**Examples:** + +Register a resource with a custom name: +```python +@server.resource("resource://my-resource") +def get_data() -> str: + return "Hello, world!" + +@server.resource("resource://my-resource") +async get_data() -> str: + data = await fetch_data() + return f"Hello, world! {data}" + +@server.resource("resource://{city}/weather") +def get_weather(city: str) -> str: + return f"Weather for {city}" + +@server.resource("resource://{city}/weather") +async def get_weather_with_context(city: str, ctx: Context) -> str: + await ctx.info(f"Fetching weather for {city}") + return f"Weather for {city}" + +@server.resource("resource://{city}/weather") +async def get_weather(city: str) -> str: + data = await fetch_weather(city) + return f"Weather for {city}: {data}" +``` + + +#### `add_prompt` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/server.py#L2091" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +add_prompt(self, prompt: Prompt | Callable[..., Any]) -> Prompt +``` + +Add a prompt to the server. + +**Args:** +- `prompt`: A Prompt instance or @prompt-decorated function to add + +**Returns:** +- The prompt instance that was added to the server. + + +#### `prompt` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/server.py#L2103" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +prompt(self, name_or_fn: F) -> F +``` + +#### `prompt` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/server.py#L2118" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +prompt(self, name_or_fn: str | None = None) -> Callable[[F], F] +``` + +#### `prompt` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/server.py#L2132" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +prompt(self, name_or_fn: str | AnyFunction | None = None) -> Callable[[AnyFunction], FunctionPrompt] | FunctionPrompt | partial[Callable[[AnyFunction], FunctionPrompt] | FunctionPrompt] +``` + +Decorator to register a prompt. + + Prompts can optionally request a Context object by adding a parameter with the + Context type annotation. The context provides access to MCP capabilities like + logging, progress reporting, and session information. + + This decorator supports multiple calling patterns: + - @server.prompt (without parentheses) + - @server.prompt() (with empty parentheses) + - @server.prompt("custom_name") (with name as first argument) + - @server.prompt(name="custom_name") (with name as keyword argument) + - server.prompt(function, name="custom_name") (direct function call) + + Args: + name_or_fn: Either a function (when used as @prompt), a string name, or None + name: Optional name for the prompt (keyword-only, alternative to name_or_fn) + description: Optional description of what the prompt does + tags: Optional set of tags for categorizing the prompt + meta: Optional meta information about the prompt + + Examples: + + ```python + @server.prompt + def analyze_table(table_name: str) -> list[Message]: + schema = read_table_schema(table_name) + return [ + { + "role": "user", + "content": f"Analyze this schema: +{schema}" + } + ] + + @server.prompt() + async def analyze_with_context(table_name: str, ctx: Context) -> list[Message]: + await ctx.info(f"Analyzing table {table_name}") + schema = read_table_schema(table_name) + return [ + { + "role": "user", + "content": f"Analyze this schema: +{schema}" + } + ] + + @server.prompt("custom_name") + async def analyze_file(path: str) -> list[Message]: + content = await read_file(path) + return [ + { + "role": "user", + "content": { + "type": "resource", + "resource": { + "uri": f"file://{path}", + "text": content + } + } + } + ] + + @server.prompt(name="custom_name") + def another_prompt(data: str) -> list[Message]: + return [{"role": "user", "content": data}] + + # Direct function call + server.prompt(my_function, name="custom_name") + ``` + + +#### `add_completion_handler` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/server.py#L2230" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +add_completion_handler(self, handler: CompletionHandler) -> None +``` + +Register the server's argument-completion handler. + +A server has a single completion handler that answers every +`completion/complete` request, switching on the reference (a prompt or +resource template) and the argument being completed. Registering it also +registers the low-level `completion/complete` handler, which is what +makes the SDK declare the completions capability — so the capability is +advertised exactly when the server can answer. Calling this again +replaces the handler. + +**Args:** +- `handler`: A callable taking the reference, the +`CompletionArgument`, and the optional `CompletionContext`, and +returning candidate values (a `Completion`, a list of strings, +or None). May be sync or async. + + +#### `completion` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/server.py#L2251" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +completion(self, handler: CompletionHandler) -> CompletionHandler +``` + +#### `completion` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/server.py#L2254" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +completion(self) -> Callable[[CompletionHandler], CompletionHandler] +``` + +#### `completion` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/server.py#L2258" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +completion(self, handler: CompletionHandler | None = None) -> CompletionHandler | Callable[[CompletionHandler], CompletionHandler] +``` + +Decorator to register the server's argument-completion handler. + +The handler answers `completion/complete` requests for prompt arguments +and resource-template parameters. It receives the reference being +completed, the argument (its name and the partial value typed so far), +and the context of arguments already supplied, and returns candidate +values. Return a list of strings, a `Completion` (to include pagination +hints), or None when the reference/argument is not one it handles — an +unhandled reference yields an empty completion, not an error. + +Registering a handler declares the completions capability; a server with +none does not advertise it. This works identically on the handshake and +modern protocol eras. + +Supports both `@mcp.completion` and `@mcp.completion()`. + +Example: + + ```python + from fastmcp import FastMCP + from mcp_types import Completion, PromptReference + + mcp = FastMCP("Completion Server") + + @mcp.prompt + def poem(theme: str) -> str: + return f"Write a poem about {theme}" + + @mcp.completion + def complete(ref, argument, context): + if isinstance(ref, PromptReference) and ref.name == "poem": + if argument.name == "theme": + options = ["nature", "love", "adventure"] + return [o for o in options if o.startswith(argument.value)] + return None + ``` + + +#### `mount` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/server.py#L2308" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +mount(self, server: FastMCP[LifespanResultT], namespace: str | None = None, tool_names: dict[str, str] | None = None) -> None +``` + +Mount another FastMCP server on this server with an optional namespace. + +Mounting establishes a dynamic connection between servers. When a client +interacts with a mounted server's objects through the parent server, requests +are forwarded to the mounted server in real-time. This means changes to the +mounted server are immediately reflected when accessed through the parent. + +When a server is mounted with a namespace: +- Tools from the mounted server are accessible with namespaced names. + Example: If server has a tool named "get_weather", it will be available as "namespace_get_weather". +- Resources are accessible with namespaced URIs. + Example: If server has a resource with URI "weather://forecast", it will be available as + "weather://namespace/forecast". +- Templates are accessible with namespaced URI templates. + Example: If server has a template with URI "weather://location/{id}", it will be available + as "weather://namespace/location/{id}". +- Prompts are accessible with namespaced names. + Example: If server has a prompt named "weather_prompt", it will be available as + "namespace_weather_prompt". + +When a server is mounted without a namespace (namespace=None), its tools, resources, templates, +and prompts are accessible with their original names. Multiple servers can be mounted +without namespaces, and they will be tried in order until a match is found. + +The mounted server's lifespan is executed when the parent server starts, and its +middleware chain is invoked for all operations (tool calls, resource reads, prompts). + +**Args:** +- `server`: The FastMCP server to mount. +- `namespace`: Optional namespace to use for the mounted server's objects. If None, +the server's objects are accessible with their original names. +- `tool_names`: Optional mapping of original tool names to custom names. Use this +to override namespaced names. Keys are the original tool names from the +mounted server. + + +#### `from_openapi` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/server.py#L2379" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +from_openapi(cls, openapi_spec: dict[str, Any], client: httpx2.AsyncClient | None = None, name: str = 'OpenAPI Server', route_maps: list[RouteMap] | None = None, route_map_fn: OpenAPIRouteMapFn | None = None, mcp_component_fn: OpenAPIComponentFn | None = None, mcp_names: dict[str, str] | None = None, tags: set[str] | None = None, validate_output: bool = True, **settings: Any) -> Self +``` + +Create a FastMCP server from an OpenAPI specification. + +**Args:** +- `openapi_spec`: OpenAPI schema as a dictionary +- `client`: Optional httpx2 AsyncClient for making HTTP requests. +If not provided, a default client is created using the first +server URL from the OpenAPI spec with a 30-second timeout. +Legacy httpx clients are temporarily accepted with a deprecation +warning. +- `name`: Name for the MCP server +- `route_maps`: Optional list of RouteMap objects defining route mappings +- `route_map_fn`: Optional callable for advanced route type mapping +- `mcp_component_fn`: Optional callable for component customization +- `mcp_names`: Optional dictionary mapping operationId to component names +- `tags`: Optional set of tags to add to all components +- `validate_output`: If True (default), tools use the output schema +extracted from the OpenAPI spec for response validation. If +False, a permissive schema is used instead, allowing any +response structure while still returning structured JSON. +- `**settings`: Additional settings passed to FastMCP + +**Returns:** +- A FastMCP server with an OpenAPIProvider attached. + + +#### `from_fastapi` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/server.py#L2432" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +from_fastapi(cls, app: Any, name: str | None = None, route_maps: list[RouteMap] | None = None, route_map_fn: OpenAPIRouteMapFn | None = None, mcp_component_fn: OpenAPIComponentFn | None = None, mcp_names: dict[str, str] | None = None, httpx_client_kwargs: dict[str, Any] | None = None, tags: set[str] | None = None, **settings: Any) -> Self +``` + +Create a FastMCP server from a FastAPI application. + +**Args:** +- `app`: FastAPI application instance +- `name`: Name for the MCP server (defaults to app.title) +- `route_maps`: Optional list of RouteMap objects defining route mappings +- `route_map_fn`: Optional callable for advanced route type mapping +- `mcp_component_fn`: Optional callable for component customization +- `mcp_names`: Optional dictionary mapping operationId to component names +- `httpx_client_kwargs`: Optional kwargs passed to httpx2.AsyncClient. +Use this to configure timeout and other client settings. +- `tags`: Optional set of tags to add to all components +- `**settings`: Additional settings passed to FastMCP + +**Returns:** +- A FastMCP server with an OpenAPIProvider attached. + + +#### `generate_name` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/server.py#L2487" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +generate_name(cls, name: str | None = None) -> str +``` diff --git a/docs/python-sdk/fastmcp-server-session_scoped_event_store.mdx b/docs/python-sdk/fastmcp-server-session_scoped_event_store.mdx new file mode 100644 index 000000000..b595a435b --- /dev/null +++ b/docs/python-sdk/fastmcp-server-session_scoped_event_store.mdx @@ -0,0 +1,31 @@ +--- +title: session_scoped_event_store +sidebarTitle: session_scoped_event_store +--- + +# `fastmcp.server.session_scoped_event_store` + + +Lightweight session scoping for Streamable HTTP event stores. + +## Classes + +### `SessionScopedEventStore` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/session_scoped_event_store.py#L19" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + + +EventStore adapter that isolates stream IDs to one transport session. + + +**Methods:** + +#### `store_event` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/session_scoped_event_store.py#L34" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +store_event(self, stream_id: StreamId, message: JSONRPCMessage | None) -> EventId +``` + +#### `replay_events_after` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/session_scoped_event_store.py#L41" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +replay_events_after(self, last_event_id: EventId, send_callback: EventCallback) -> StreamId | None +``` diff --git a/docs/python-sdk/fastmcp-server-sessions.mdx b/docs/python-sdk/fastmcp-server-sessions.mdx new file mode 100644 index 000000000..0caea6b13 --- /dev/null +++ b/docs/python-sdk/fastmcp-server-sessions.mdx @@ -0,0 +1,319 @@ +--- +title: sessions +sidebarTitle: sessions +--- + +# `fastmcp.server.sessions` + + +Stateless session state: server-side per-user and per-session storage. + +Modern (2026-07-28) MCP connections are stateless by construction — every +request builds a fresh connection whose in-memory state is discarded when the +request returns. This module gives tools two explicit ways to keep state across +calls, both backed by the server's existing state store and both isolated by the +authenticated principal rather than by any client-declared identifier. + +- `Session`: async `get`/`set`/`delete`/`clear` over a single dict stored under + one key, scoped to a `(principal, session_id)` pair. This is the state-accessor + object a handler works with — the value the standalone `get_session(id)` + returns and the value injected for a `UserSession` parameter. +- `session: UserSession` (injected): a per-user bucket, dependency-injected like + `ctx: Context` and keyed by the request's authenticated principal. Requires + auth. `UserSession` is the injection annotation; the injected value is a + `Session`. It is always available under auth — no `create_session`, no + provider, no validation. +- `session_id: SessionId` (argument): a required string the agent supplies, + resolved with the standalone `await get_session(session_id)`. The id is + minted + by `create_session`; an id that was never created (or was created under a + different principal) is rejected. This validation is the whole guarantee — an + unminted id never resolves, so nothing enforces provider registration. +- `SessionProvider`: a `Provider` contributing `create_session` / `end_session` + tools. Register it with `mcp.add_provider(SessionProvider())` so a tool that + takes `session_id` has a way to mint ids; without it, no id can be created, so + those tools simply cannot resolve a session. + +Isolation is the authenticated principal, not the session id. State keyed by +`(principal, session_id)` means a request under principal B can never address +principal A's keys, no matter what `session_id` it passes; the id only organizes +sessions within a principal. Without auth there is no principal wall — a session +id is a bearer capability and sessions are not a boundary between clients. + + +## Functions + +### `current_principal` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/sessions.py#L139" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +current_principal() -> str | None +``` + + +The authenticated principal for the current request as a compact JSON string. + +Returns the `(client_id, issuer, subject)` triple encoded as compact JSON, or +`None` on an unauthenticated request. Two users of one OAuth client are +distinct principals whenever the token verifier supplies a subject. + + +### `session_storage_key` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/sessions.py#L164" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +session_storage_key(principal: str | None, session_id: str) -> str +``` + + +The single storage key holding a session's state dict. + +Keyed by `(principal, session_id)`: the principal is the isolation wall, the +id organizes sessions within it. A session's whole state lives under this one +key as a dict, so one key means one store TTL per session and `end` is a +single delete. + + +### `session_id_parameter_names` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/sessions.py#L343" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +session_id_parameter_names(fn: Callable[..., object]) -> tuple[str, ...] +``` + + +Names of a function's parameters annotated with `SessionId`. + +Scans resolved type hints for `Annotated[str, _SessionIdMarker()]` metadata. +Returns an empty tuple when the hints cannot be resolved (the function then +simply carries no auto-populated session-id description). + +`functools.partial` is unwrapped first, since `get_type_hints` rejects a +partial object — FastMCP supports registering a partial as a tool, and its +schema is still built from the underlying function, so its `SessionId` +parameters must be detected here too. Parameters the partial has already +bound — positionally or by keyword — are dropped, matching the tool's actual +argument surface (the partial's own signature already reflects this). + + +### `CurrentSession` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/sessions.py#L449" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +CurrentSession() -> Session +``` + + +Inject the per-user `Session` for the current authenticated principal. + +Rarely written explicitly — a `session: UserSession` parameter is rewritten +to this. Provided for parity with `CurrentContext()` when an explicit default +is preferred. + + +### `OptionalCurrentSession` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/sessions.py#L459" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +OptionalCurrentSession() -> Session | None +``` + + +Inject the per-user `Session`, or `None` when the request is unauthenticated. + +Rarely written explicitly — a `session: UserSession | None = None` parameter +is rewritten to this. Provided for parity with `OptionalCurrentContext()`. + + +### `create_session` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/sessions.py#L468" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +create_session() -> str +``` + + +Create a new session and return its identifier. + +Mints an unguessable `uuid4`, records an initial session owned by the current +principal, and returns the id as a string. Store it and pass it back as a +`session_id` argument on later calls to persist state across a session — only +an id created this way resolves. State is keyed by the authenticated +principal, so the id organizes sessions within a user; on an unauthenticated +connection the id is the only thing standing between callers, which is why it +is unguessable. + + +### `end_session` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/sessions.py#L490" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +end_session(session_id: SessionId) -> str +``` + + +End a session and delete all of its state. + +Validates the id like any other resolution (an unknown or foreign id is +rejected), then deletes the session's key so the id no longer resolves. + + +## Classes + +### `SessionAuthError` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/sessions.py#L106" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + + +An injected `session: UserSession` was requested with no authenticated principal. + +Per-user session injection keys off the request's authenticated principal, so +it is only meaningful under auth. A tool that needs cross-call state without +auth should take a `session_id: SessionId` argument instead. + + +### `InvalidSession` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/sessions.py#L125" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + + +A session id did not resolve to a session created under the current principal. + +Raised by `get_session(session_id)` when the id was never created, or was +created under a different principal. The public message is deliberately +generic — the specific reason (which id, which principal) is logged at debug +level, not returned to the caller, so an attacker cannot distinguish "unknown +id" from "belongs to someone else". + + +### `Session` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/sessions.py#L175" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + + +Async accessors over one `(principal, session_id)` bucket of state. + +A session's state is a single dict stored under one key. That dict holds user +state in a `state` sub-dict and a small creation marker alongside it, so a +created-but-empty session is still distinguishable from a missing one. +`get`/`set`/`delete` read-modify-write the sub-dict; `clear` empties the +sub-dict but keeps the session valid; `end` deletes the whole key. Writes +never impose a TTL — retention is entirely the server store's (configure it on +the store you pass to `FastMCP(session_state_store=...)`). + +Concurrent writes to one session race on the read-modify-write; session state +is small and typically driven serially by one agent, so this is acceptable. + + +**Methods:** + +#### `id` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/sessions.py#L205" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +id(self) -> str | None +``` + +The session's identifier, or `None` for an injected per-user session. + +For a session resolved from a `session_id` argument (or minted by +`create_session`) this is that id. An injected `UserSession` has no +distinct id — its bucket is the authenticated user — so it is `None`; the +internal principal-derived key is deliberately not exposed here. + + +#### `get` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/sessions.py#L254" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +get(self, key: str, default: Any = None) -> Any +``` + +Return the value for `key`, or `default` when it is not set. + + +#### `set` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/sessions.py#L259" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +set(self, key: str, value: Any) -> None +``` + +Store `value` under `key` in this session (read-modify-write). + +Preserves the creation marker: only the user-state sub-dict is touched. + + +#### `delete` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/sessions.py#L270" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +delete(self, key: str) -> None +``` + +Remove `key` from this session, if present (preserves the marker). + + +#### `clear` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/sessions.py#L281" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +clear(self) -> None +``` + +Empty the session's user state but keep the session valid. + +The user-state sub-dict is reset to empty while the creation marker stays +in place, so a cleared session still resolves through `get_session`. +To invalidate a session entirely, use `end` (what `end_session` calls). + + +#### `end` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/sessions.py#L294" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +end(self) -> None +``` + +Invalidate the session — delete its one key and all of its state. + +After this the id no longer resolves through `get_session`. This is +what `end_session` calls; `clear` only empties state and keeps the session. + + +### `UserSession` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/sessions.py#L303" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + + +Annotation marker for the injected per-user session. + +A `session: UserSession` parameter is **dependency-injected** like +`ctx: Context`: keyed by the request's authenticated principal, excluded from +the input schema, and requiring auth (it raises `SessionAuthError` with no +principal). It doubles as the injection *annotation* and the injected +type — the value a handler receives is a `UserSession`, which subclasses +`Session`, so `await session.get(...)`, `.set`, `.delete`, and `.clear` all +work exactly as on any other `Session`. + +Unlike `session_id: SessionId`, the per-user bucket needs no `create_session`, +no `SessionProvider`, and no validation — it is always available under auth, +keyed directly by the caller's identity. + +```python +from fastmcp.server.sessions import UserSession + +@mcp.tool +async def remember(fact: str, session: UserSession) -> str: + await session.set("fact", fact) + return "noted" +``` + +Subclasses `Session` only so the framework's type-based injection detector can +key off it; it adds no behavior of its own. + + +### `SessionProvider` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/sessions.py#L501" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + + +Provider contributing the session lifecycle tools. + +Register it whenever a tool declares a `session_id: SessionId` argument: + +```python +from fastmcp.server.sessions import SessionProvider + +mcp.add_provider(SessionProvider()) +``` + +It registers two tools: + +- `create_session()` mints an unguessable `uuid4`, records the session, and + returns the id. +- `end_session(session_id)` invalidates that session and deletes its state. + +It owns no storage (session state lives in the server's configured +`session_state_store`) and imposes no TTL (retention is the store's). It +exists to mint and end owned session ids. Registration is not enforced: with +no provider, no id can be created, so every `get_session(...)` rejects — +a `session_id` tool without a provider simply cannot resolve a session. + diff --git a/docs/python-sdk/fastmcp-server-telemetry.mdx b/docs/python-sdk/fastmcp-server-telemetry.mdx new file mode 100644 index 000000000..874fcf1e9 --- /dev/null +++ b/docs/python-sdk/fastmcp-server-telemetry.mdx @@ -0,0 +1,117 @@ +--- +title: telemetry +sidebarTitle: telemetry +--- + +# `fastmcp.server.telemetry` + + +Server-side telemetry helpers. + +## Functions + +### `get_auth_span_attributes` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/telemetry.py#L43" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +get_auth_span_attributes() -> dict[str, str] +``` + + +Get auth attributes for the current request, if authenticated. + + +### `get_session_span_attributes` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/telemetry.py#L60" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +get_session_span_attributes() -> dict[str, str] +``` + + +Get session attributes for the current request. + + +### `get_protocol_span_attributes` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/telemetry.py#L74" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +get_protocol_span_attributes() -> dict[str, str] +``` + + +Get the negotiated MCP protocol version for the current request. + +Mirrors the `mcp.protocol.version` attribute the SDK's own +`OpenTelemetryMiddleware` sets — FastMCP drops that middleware to avoid a +duplicate SERVER span, so this restores the attribute on FastMCP's span. + + +### `record_span_exception` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/telemetry.py#L154" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +record_span_exception(span: Span, e: Exception) -> None +``` + + +Record an exception and error status on a span. + + +### `seam_span` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/telemetry.py#L164" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +seam_span(method: str, server_name: str) -> Generator[Span, None, None] +``` + + +Open the per-request SERVER span at the FastMCP middleware seam. + +The span is named after the method and carries the base MCP attributes +(`mcp.method.name`, `fastmcp.server.name`, auth/session context) so +seam-only methods (`logging/setLevel`, `tasks/*`, `ping`, `initialize`, ...) +are fully attributed even though they never reach the high-level path. It is +marked with `SEAM_SPAN_MARKER` so a later `server_span` call in the +high-level path enriches this span with component attributes instead of +opening a second one. Exceptions raised anywhere below the seam — including +rejections *before* the high-level path (auth, not-found, middleware vetoes) +that would otherwise produce no SERVER span at all — are recorded here. + +In `propagation_only` mode no span is opened at all — this is the one place +that has to know the difference, because the seam is where the incoming +`_meta` parent context is applied for the whole request. + + +### `server_span` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/telemetry.py#L224" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +server_span(name: str, method: str, server_name: str, component_type: str, component_key: str, resource_uri: str | None = None, tool_name: str | None = None, prompt_name: str | None = None) -> Generator[Span, None, None] +``` + + +Emit or enrich a SERVER span with standard MCP attributes and auth context. + +When the current active span is the request's seam span (opened by +`FastMCPServerMiddleware` and marked with `SEAM_SPAN_MARKER`), this sets the +component attributes on that span and yields it *without* starting a second +span — so failures rejected before this point and the successful high-level +call share one richly-attributed SERVER span. Otherwise (non-seam contexts, +e.g. in-process `mcp.call_tool()` calls that bypass the dispatcher) it opens a +new SERVER span as before. + +Automatically records any exception on the span and sets error status. + +In `propagation_only` mode no span is opened or enriched. The seam has +normally already attached the incoming parent context for this request; +doing it again here is a no-op, and covers the in-process callers that +bypass the dispatcher and so never reach the seam at all. + + +### `delegate_span` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/telemetry.py#L305" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +delegate_span(name: str, provider_type: str, component_key: str, method: str | None = None) -> Generator[Span, None, None] +``` + + +Create an INTERNAL span for provider delegation. + +Used by FastMCPProvider when delegating to mounted servers. +Automatically records any exception on the span and sets error status. + diff --git a/docs/python-sdk/fastmcp-server-transforms.mdx b/docs/python-sdk/fastmcp-server-transforms.mdx new file mode 100644 index 000000000..7e6d19054 --- /dev/null +++ b/docs/python-sdk/fastmcp-server-transforms.mdx @@ -0,0 +1,193 @@ +--- +title: transforms +sidebarTitle: transforms +--- + +# `fastmcp.server.transforms` + + +Transform system for component transformations. + +Transforms modify components (tools, resources, prompts). List operations use a pure +function pattern where transforms receive sequences and return transformed sequences. +Get operations use a middleware pattern with `call_next` to chain lookups. + +Unlike middleware (which operates on requests), transforms are observable by the +system for task registration, tag filtering, and component introspection. + +Example: + ```python + from fastmcp import FastMCP + from fastmcp.server.transforms import Namespace + + server = FastMCP("Server") + mount = server.mount(other_server) + mount.add_transform(Namespace("api")) # Tools become api_toolname + ``` + + +## Classes + +### `GetToolNext` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/transforms/__init__.py#L36" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + + +Protocol for get_tool call_next functions. + + +### `GetResourceNext` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/transforms/__init__.py#L44" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + + +Protocol for get_resource call_next functions. + + +### `GetResourceTemplateNext` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/transforms/__init__.py#L52" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + + +Protocol for get_resource_template call_next functions. + + +### `GetPromptNext` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/transforms/__init__.py#L60" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + + +Protocol for get_prompt call_next functions. + + +### `Transform` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/transforms/__init__.py#L68" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + + +Base class for component transformations. + +List operations use a pure function pattern: transforms receive sequences +and return transformed sequences. Get operations use a middleware pattern +with `call_next` to chain lookups. + + +**Methods:** + +#### `list_tools` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/transforms/__init__.py#L95" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +list_tools(self, tools: Sequence[Tool]) -> Sequence[Tool] +``` + +List tools with transformation applied. + +**Args:** +- `tools`: Sequence of tools to transform. + +**Returns:** +- Transformed sequence of tools. + + +#### `get_tool` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/transforms/__init__.py#L106" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +get_tool(self, name: str, call_next: GetToolNext) -> Tool | None +``` + +Get a tool by name. + +**Args:** +- `name`: The requested tool name (may be transformed). +- `call_next`: Callable to get tool from downstream. +- `version`: Optional version filter to apply. + +**Returns:** +- The tool if found, None otherwise. + + +#### `list_resources` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/transforms/__init__.py#L125" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +list_resources(self, resources: Sequence[Resource]) -> Sequence[Resource] +``` + +List resources with transformation applied. + +**Args:** +- `resources`: Sequence of resources to transform. + +**Returns:** +- Transformed sequence of resources. + + +#### `get_resource` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/transforms/__init__.py#L136" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +get_resource(self, uri: str, call_next: GetResourceNext) -> Resource | None +``` + +Get a resource by URI. + +**Args:** +- `uri`: The requested resource URI (may be transformed). +- `call_next`: Callable to get resource from downstream. +- `version`: Optional version filter to apply. + +**Returns:** +- The resource if found, None otherwise. + + +#### `list_resource_templates` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/transforms/__init__.py#L159" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +list_resource_templates(self, templates: Sequence[ResourceTemplate]) -> Sequence[ResourceTemplate] +``` + +List resource templates with transformation applied. + +**Args:** +- `templates`: Sequence of resource templates to transform. + +**Returns:** +- Transformed sequence of resource templates. + + +#### `get_resource_template` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/transforms/__init__.py#L172" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +get_resource_template(self, uri: str, call_next: GetResourceTemplateNext) -> ResourceTemplate | None +``` + +Get a resource template by URI. + +**Args:** +- `uri`: The requested template URI (may be transformed). +- `call_next`: Callable to get template from downstream. +- `version`: Optional version filter to apply. + +**Returns:** +- The resource template if found, None otherwise. + + +#### `list_prompts` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/transforms/__init__.py#L195" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +list_prompts(self, prompts: Sequence[Prompt]) -> Sequence[Prompt] +``` + +List prompts with transformation applied. + +**Args:** +- `prompts`: Sequence of prompts to transform. + +**Returns:** +- Transformed sequence of prompts. + + +#### `get_prompt` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/transforms/__init__.py#L206" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +get_prompt(self, name: str, call_next: GetPromptNext) -> Prompt | None +``` + +Get a prompt by name. + +**Args:** +- `name`: The requested prompt name (may be transformed). +- `call_next`: Callable to get prompt from downstream. +- `version`: Optional version filter to apply. + +**Returns:** +- The prompt if found, None otherwise. + diff --git a/docs/python-sdk/fastmcp-settings.mdx b/docs/python-sdk/fastmcp-settings.mdx index b6f401939..a0d999d27 100644 --- a/docs/python-sdk/fastmcp-settings.mdx +++ b/docs/python-sdk/fastmcp-settings.mdx @@ -7,7 +7,7 @@ sidebarTitle: settings ## Classes -### `Settings` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/settings.py#L32" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> +### `Settings` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/settings.py#L34" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> FastMCP settings. @@ -15,7 +15,7 @@ FastMCP settings. **Methods:** -#### `get_setting` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/settings.py#L44" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> +#### `get_setting` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/settings.py#L46" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> ```python get_setting(self, attr: str) -> Any @@ -25,7 +25,7 @@ Get a setting. If the setting contains one or more `__`, it will be treated as a nested setting. -#### `set_setting` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/settings.py#L57" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> +#### `set_setting` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/settings.py#L59" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> ```python set_setting(self, attr: str, value: Any) -> None @@ -35,7 +35,7 @@ Set a setting. If the setting contains one or more `__`, it will be treated as a nested setting. -#### `normalize_log_level` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/settings.py#L79" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> +#### `normalize_log_level` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/settings.py#L81" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> ```python normalize_log_level(cls, v) diff --git a/docs/python-sdk/fastmcp-telemetry.mdx b/docs/python-sdk/fastmcp-telemetry.mdx index 8e034ff6e..3cb06ca12 100644 --- a/docs/python-sdk/fastmcp-telemetry.mdx +++ b/docs/python-sdk/fastmcp-telemetry.mdx @@ -31,7 +31,52 @@ Example usage with SDK: ## Functions -### `get_tracer` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/telemetry.py#L81" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> +### `telemetry_mode` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/telemetry.py#L86" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +telemetry_mode() -> 'TelemetryMode' +``` + + +Resolve the effective telemetry mode for the current context. + +This is `fastmcp.settings.telemetry_mode`, except that an active +`suppress_fastmcp_telemetry()` block downgrades `native` to +`propagation_only`. Suppression never upgrades or overrides `off`: `off` +means FastMCP touches nothing, and a narrower request to skip FastMCP's +spans cannot re-enable the context propagation `off` deliberately omits. + + +### `native_spans_enabled` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/telemetry.py#L103" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +native_spans_enabled() -> bool +``` + + +Whether FastMCP should create its own spans right now. + + +### `suppress_fastmcp_telemetry` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/telemetry.py#L109" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +suppress_fastmcp_telemetry() -> Iterator[None] +``` + + +Suppress FastMCP's own spans without disabling trace propagation. + +Scoped equivalent of `telemetry_mode="propagation_only"`, for callers that +embed FastMCP inside their own instrumented stack and want to own the MCP +span hierarchy for a specific block. Narrower than OpenTelemetry's global +instrumentation suppression: only FastMCP's spans are skipped, so nested +instrumentation (HTTP clients, databases) keeps emitting, and trace context +still flows through `_meta` so those spans are parented correctly. + +Has no effect when `telemetry_mode` is already `off`. + + +### `get_tracer` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/telemetry.py#L128" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> ```python get_tracer(version: str | None = None) -> Tracer @@ -42,21 +87,22 @@ Get the FastMCP tracer for creating spans. Instrumentation is on by default. FastMCP uses only the OpenTelemetry API, so span creation is a no-op with negligible overhead unless an OpenTelemetry -SDK and exporter are configured. Set `fastmcp.settings.enable_telemetry` to -False (env `FASTMCP_ENABLE_TELEMETRY=false`) to turn instrumentation off -entirely, in which case this returns a pass-through tracer that leaves the -current OTel context untouched even when an SDK is configured. +SDK and exporter are configured. When `fastmcp.settings.telemetry_mode` is +`propagation_only` or `off` — or the caller is inside a +`suppress_fastmcp_telemetry()` block — this returns a pass-through tracer +that creates no spans and leaves the current OTel context untouched even +when an SDK is configured. **Args:** - `version`: Optional version string for the instrumentation **Returns:** -- A tracer instance. Returns a non-attaching pass-through tracer if -- telemetry is disabled; span creation is otherwise a no-op unless an SDK -- is configured. +- A tracer instance. Returns a non-attaching pass-through tracer when +- FastMCP's own spans are disabled; span creation is otherwise a no-op +- unless an SDK is configured. -### `inject_trace_context` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/telemetry.py#L106" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> +### `inject_trace_context` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/telemetry.py#L152" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> ```python inject_trace_context(meta: dict[str, Any] | None = None) -> dict[str, Any] | None @@ -73,7 +119,7 @@ Inject current trace context into a meta dict for MCP request propagation. - or None if no trace context to inject and meta was None -### `record_span_error` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/telemetry.py#L132" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> +### `record_span_error` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/telemetry.py#L183" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> ```python record_span_error(span: Span, exception: BaseException) -> None @@ -83,7 +129,7 @@ record_span_error(span: Span, exception: BaseException) -> None Record an exception on a span and set error status. -### `restore_dropped_attributes` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/telemetry.py#L158" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> +### `restore_dropped_attributes` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/telemetry.py#L209" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> ```python restore_dropped_attributes(span: Span, attrs: Mapping[str, otel_types.AttributeValue]) -> None @@ -133,7 +179,7 @@ kept at call sites so it reads alongside the sibling `is_recording()` guards already in those functions. -### `extract_trace_context` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/telemetry.py#L212" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> +### `extract_trace_context` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/telemetry.py#L263" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> ```python extract_trace_context(meta: dict[str, Any] | None) -> Context diff --git a/docs/python-sdk/fastmcp-utilities-docstring_parsing.mdx b/docs/python-sdk/fastmcp-utilities-docstring_parsing.mdx index b8cacc823..bceb0256e 100644 --- a/docs/python-sdk/fastmcp-utilities-docstring_parsing.mdx +++ b/docs/python-sdk/fastmcp-utilities-docstring_parsing.mdx @@ -16,7 +16,7 @@ callers. ## Functions -### `parse_docstring` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/docstring_parsing.py#L35" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> +### `parse_docstring` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/docstring_parsing.py#L33" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> ```python parse_docstring(fn: Callable[..., Any]) -> ParsedDocstring @@ -32,7 +32,7 @@ docstring as the description with no parameter descriptions. ## Classes -### `ParsedDocstring` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/docstring_parsing.py#L28" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> +### `ParsedDocstring` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/docstring_parsing.py#L26" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> The extracted description and per-parameter descriptions from a docstring. diff --git a/docs/python-sdk/fastmcp-utilities-exceptions.mdx b/docs/python-sdk/fastmcp-utilities-exceptions.mdx index 169e66d65..129ad5a67 100644 --- a/docs/python-sdk/fastmcp-utilities-exceptions.mdx +++ b/docs/python-sdk/fastmcp-utilities-exceptions.mdx @@ -7,13 +7,53 @@ sidebarTitle: exceptions ## Functions -### `iter_exc` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/exceptions.py#L36" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> +### `is_http_status_error` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/exceptions.py#L19" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +is_http_status_error(exc: BaseException) -> bool +``` + + +Return whether an exception is an httpx2 or legacy-httpx status error. + + +### `get_http_status_code` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/exceptions.py#L26" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +get_http_status_code(exc: BaseException) -> int | None +``` + + +Return the response status code from a recognized HTTP status error. + + +### `is_timeout_error` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/exceptions.py#L34" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +is_timeout_error(exc: BaseException) -> bool +``` + + +Return whether an exception is an httpx2 or legacy-httpx timeout. + + +### `is_request_error` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/exceptions.py#L41" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +is_request_error(exc: BaseException) -> bool +``` + + +Return whether an exception is an httpx2 or legacy-httpx request error. + + +### `iter_exc` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/exceptions.py#L48" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> ```python iter_exc(group: BaseExceptionGroup) ``` -### `get_catch_handlers` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/exceptions.py#L64" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> +### `get_catch_handlers` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/exceptions.py#L76" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> ```python get_catch_handlers() -> Mapping[type[BaseException] | Iterable[type[BaseException]], Callable[[BaseExceptionGroup[Any]], Any]] diff --git a/docs/python-sdk/fastmcp-utilities-inspect.mdx b/docs/python-sdk/fastmcp-utilities-inspect.mdx index fca4da868..d2aca51d1 100644 --- a/docs/python-sdk/fastmcp-utilities-inspect.mdx +++ b/docs/python-sdk/fastmcp-utilities-inspect.mdx @@ -42,7 +42,7 @@ Extract information from a FastMCP v1.x instance using a Client. - FastMCPInfo dataclass containing the extracted information -### `inspect_fastmcp` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/inspect.py#L411" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> +### `inspect_fastmcp` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/inspect.py#L413" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> ```python inspect_fastmcp(mcp: FastMCP[Any] | SDKServer) -> FastMCPInfo @@ -61,7 +61,7 @@ and uses the appropriate extraction method. - FastMCPInfo dataclass containing the extracted information -### `format_fastmcp_info` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/inspect.py#L436" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> +### `format_fastmcp_info` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/inspect.py#L438" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> ```python format_fastmcp_info(info: FastMCPInfo) -> bytes @@ -73,7 +73,7 @@ Format FastMCPInfo as FastMCP-specific JSON. This includes FastMCP-specific fields like tags, enabled, annotations, etc. -### `format_mcp_info` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/inspect.py#L465" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> +### `format_mcp_info` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/inspect.py#L467" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> ```python format_mcp_info(mcp: FastMCP[Any] | SDKServer) -> bytes @@ -86,7 +86,7 @@ Uses Client to get the standard MCP protocol format with camelCase fields. Includes version metadata at the top level. -### `format_info` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/inspect.py#L500" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> +### `format_info` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/inspect.py#L502" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> ```python format_info(mcp: FastMCP[Any] | SDKServer, format: InspectFormat | Literal['fastmcp', 'mcp'], info: FastMCPInfo | None = None) -> bytes @@ -136,7 +136,7 @@ Information about a resource template. Information extracted from a FastMCP instance. -### `InspectFormat` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/inspect.py#L429" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> +### `InspectFormat` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/inspect.py#L431" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> Output format for inspect command. diff --git a/docs/python-sdk/fastmcp-utilities-json_schema.mdx b/docs/python-sdk/fastmcp-utilities-json_schema.mdx index 8f8e580bf..654108a63 100644 --- a/docs/python-sdk/fastmcp-utilities-json_schema.mdx +++ b/docs/python-sdk/fastmcp-utilities-json_schema.mdx @@ -7,7 +7,17 @@ sidebarTitle: json_schema ## Functions -### `require_discriminator_property` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/json_schema.py#L149" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> +### `replace_refs` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/json_schema.py#L7" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +replace_refs(*args: Any, **kwargs: Any) -> Any +``` + + +Call jsonref lazily while preserving the module's patchable boundary. + + +### `require_discriminator_property` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/json_schema.py#L154" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> ```python require_discriminator_property(schema: dict[str, Any]) -> dict[str, Any] @@ -24,7 +34,7 @@ model with ``union_tag_not_found``. No-op if there is no string ``propertyName``. -### `dereference_refs` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/json_schema.py#L180" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> +### `dereference_refs` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/json_schema.py#L185" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> ```python dereference_refs(schema: dict[str, Any]) -> dict[str, Any] @@ -57,7 +67,7 @@ schemas from untrusted servers. - when no longer needed -### `resolve_root_ref` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/json_schema.py#L327" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> +### `resolve_root_ref` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/json_schema.py#L336" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> ```python resolve_root_ref(schema: dict[str, Any]) -> dict[str, Any] @@ -79,7 +89,7 @@ the referenced definition while preserving $defs for nested references. - if no resolution is needed -### `compress_schema` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/json_schema.py#L741" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> +### `compress_schema` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/json_schema.py#L750" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> ```python compress_schema(schema: dict[str, Any], prune_params: list[str] | None = None, prune_additional_properties: bool = False, prune_titles: bool = False, dereference: bool = False) -> dict[str, Any] diff --git a/docs/python-sdk/fastmcp-utilities-logging.mdx b/docs/python-sdk/fastmcp-utilities-logging.mdx index f3b58bf7e..4dde3d509 100644 --- a/docs/python-sdk/fastmcp-utilities-logging.mdx +++ b/docs/python-sdk/fastmcp-utilities-logging.mdx @@ -10,7 +10,7 @@ Logging utilities for FastMCP. ## Functions -### `get_logger` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/logging.py#L14" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> +### `get_logger` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/logging.py#L27" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> ```python get_logger(name: str) -> logging.Logger @@ -26,7 +26,7 @@ Get a logger nested under FastMCP namespace. - a configured logger instance -### `configure_logging` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/logging.py#L29" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> +### `configure_logging` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/logging.py#L42" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> ```python configure_logging(level: Literal['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'] | int = 'INFO', logger: logging.Logger | None = None, enable_rich_tracebacks: bool | None = None, **rich_kwargs: Any) -> None @@ -41,7 +41,7 @@ Configure logging for FastMCP. - `rich_kwargs`: the parameters to use for creating RichHandler -### `temporary_log_level` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/logging.py#L117" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> +### `temporary_log_level` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/logging.py#L127" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> ```python temporary_log_level(level: str | None, logger: logging.Logger | None = None, enable_rich_tracebacks: bool | None = None, **rich_kwargs: Any) diff --git a/docs/python-sdk/fastmcp-utilities-prefab.mdx b/docs/python-sdk/fastmcp-utilities-prefab.mdx new file mode 100644 index 000000000..b03d7b185 --- /dev/null +++ b/docs/python-sdk/fastmcp-utilities-prefab.mdx @@ -0,0 +1,61 @@ +--- +title: prefab +sidebarTitle: prefab +--- + +# `fastmcp.utilities.prefab` + + +Lazy helpers for FastMCP's optional Prefab UI integration. + +## Functions + +### `prefab_available` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/prefab.py#L12" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +prefab_available() -> bool +``` + + +Return whether Prefab UI is installed without importing it. + + +### `is_prefab_type` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/prefab.py#L42" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +is_prefab_type(candidate: Any) -> bool +``` + + +Return whether a type is a Prefab app or component type. + + +### `is_prefab_app` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/prefab.py#L51" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +is_prefab_app(value: Any) -> bool +``` + + +Return whether a value is a Prefab app. + + +### `is_prefab_component` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/prefab.py#L60" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +is_prefab_component(value: Any) -> bool +``` + + +Return whether a value is a Prefab component. + + +### `prefab_app_from_component` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/prefab.py#L69" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> + +```python +prefab_app_from_component(component: Any) -> Any +``` + + +Wrap a Prefab component in a Prefab app. + From 04f9971120382f50ea563f142d2ad971a846a686 Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Thu, 6 Aug 2026 19:59:19 -0400 Subject: [PATCH 49/53] Delegate typed tool output serialization to Pydantic (#4771) --- fastmcp_slim/fastmcp/tools/base.py | 90 +++++---- .../fastmcp/tools/function_parsing.py | 82 ++++---- fastmcp_slim/fastmcp/tools/function_tool.py | 1 - fastmcp_slim/fastmcp/tools/tool_transform.py | 39 +--- tests/tools/tool/test_output_schema.py | 4 + tests/tools/tool/test_results.py | 180 +++++++++--------- .../tool_transform/test_tool_transform.py | 26 ++- 7 files changed, 202 insertions(+), 220 deletions(-) diff --git a/fastmcp_slim/fastmcp/tools/base.py b/fastmcp_slim/fastmcp/tools/base.py index ccba1f365..9e11ad5c9 100644 --- a/fastmcp_slim/fastmcp/tools/base.py +++ b/fastmcp_slim/fastmcp/tools/base.py @@ -1,5 +1,6 @@ from __future__ import annotations +import inspect from collections.abc import Callable from typing import ( TYPE_CHECKING, @@ -20,7 +21,13 @@ from mcp_types import ( ToolExecution, ) from mcp_types import Tool as MCPTool -from pydantic import BaseModel, Field, PrivateAttr, model_validator +from pydantic import ( + BaseModel, + Field, + PrivateAttr, + PydanticSchemaGenerationError, + model_validator, +) from pydantic.json_schema import SkipJsonSchema from fastmcp.utilities.authorization import AuthCheck @@ -38,6 +45,7 @@ from fastmcp.utilities.types import ( Image, NotSet, NotSetT, + get_cached_typeadapter, ) if TYPE_CHECKING: @@ -48,6 +56,8 @@ if TYPE_CHECKING: logger = get_logger(__name__) +_JSONABLE_ADAPTER = get_cached_typeadapter(Any) + def _default_title(name: str) -> str: """Derive a display title from a tool name. @@ -59,34 +69,27 @@ def _default_title(name: str) -> str: return name.replace("_", " ").replace("-", " ").title() -def resolve_serialize_by_alias(value: Any) -> bool: - """Resolve the effective ``by_alias`` setting for serializing *value*. - - Pydantic's low-level serialization helpers (``to_json``, - ``to_jsonable_python``) default ``by_alias`` to ``True``, which silently - ignores a model's ``serialize_by_alias`` config. When *value* is a Pydantic - model we consult that config instead, falling back to ``True`` to preserve - FastMCP's longstanding default of emitting aliases when no preference is - declared. - """ - if isinstance(value, type): - model = value if issubclass(value, BaseModel) else None - elif isinstance(value, BaseModel): - model = type(value) - else: - model = None - - if model is None: - return True - - configured = model.model_config.get("serialize_by_alias") - return True if configured is None else configured - - def default_serializer(data: Any) -> str: - return pydantic_core.to_json( - data, fallback=str, by_alias=resolve_serialize_by_alias(data) - ).decode() + return _JSONABLE_ADAPTER.dump_json(data, fallback=str).decode() + + +def _serialize_to_jsonable(data: Any, annotation: Any = Any) -> Any: + """Serialize through Pydantic, falling back for unsupported annotations.""" + if ( + annotation is inspect.Signature.empty + or annotation is None + or annotation is Any + or annotation is ... + or isinstance(annotation, str) + ): + adapter = _JSONABLE_ADAPTER + else: + try: + return get_cached_typeadapter(annotation).dump_python(data, mode="json") + except PydanticSchemaGenerationError: + adapter = _JSONABLE_ADAPTER + + return adapter.dump_python(data, mode="json") class ToolResult(BaseModel): @@ -133,10 +136,7 @@ class ToolResult(BaseModel): ) try: - structured_content = pydantic_core.to_jsonable_python( - value=structured_content, - by_alias=resolve_serialize_by_alias(structured_content), - ) + structured_content = _serialize_to_jsonable(structured_content) except pydantic_core.PydanticSerializationError as e: logger.error( f"Could not serialize structured content. If this is unexpected, set your tool's output_schema to None to disable automatic serialization: {e}" @@ -233,6 +233,7 @@ class Tool(FastMCPComponent): KEY_PREFIX: ClassVar[str] = "tool" + return_type: Annotated[SkipJsonSchema[Any], Field(exclude=True)] = None parameters: Annotated[ dict[str, Any], Field(description="JSON schema for tool parameters") ] @@ -392,24 +393,29 @@ class Tool(FastMCPComponent): if isinstance(raw_value, bytes): return ToolResult(content=content) + is_content_result = isinstance( + raw_value, ContentBlock | Audio | Image | File + ) or ( + isinstance(raw_value, list | tuple) + and any( + isinstance(item, ContentBlock | Audio | Image | File) + for item in raw_value + ) + ) + # Skip structured content for ContentBlock types only if no output_schema # (if output_schema exists, MCP SDK requires structured_content) - if self.output_schema is None and ( - isinstance(raw_value, ContentBlock | Audio | Image | File) - or ( - isinstance(raw_value, list | tuple) - and any(isinstance(item, ContentBlock) for item in raw_value) - ) - ): + if self.output_schema is None and is_content_result: return ToolResult(content=content) try: - structured = pydantic_core.to_jsonable_python( - raw_value, by_alias=resolve_serialize_by_alias(raw_value) - ) + structured = _serialize_to_jsonable(raw_value, self.return_type) except (pydantic_core.PydanticSerializationError, UnicodeDecodeError): return ToolResult(content=content) + if not is_content_result: + content = _convert_to_content(structured) + if self.output_schema is None: # No schema - only use structured_content for dicts if isinstance(structured, dict): diff --git a/fastmcp_slim/fastmcp/tools/function_parsing.py b/fastmcp_slim/fastmcp/tools/function_parsing.py index 5972e7d0d..72d594794 100644 --- a/fastmcp_slim/fastmcp/tools/function_parsing.py +++ b/fastmcp_slim/fastmcp/tools/function_parsing.py @@ -10,11 +10,13 @@ from dataclasses import dataclass from typing import Annotated, Any, Generic, Union, get_args, get_origin, get_type_hints import mcp_types -from pydantic import BaseModel, PydanticSchemaGenerationError +from pydantic import PydanticSchemaGenerationError +from pydantic.json_schema import GenerateJsonSchema, JsonSchemaValue +from pydantic_core import core_schema from typing_extensions import TypeAliasType from typing_extensions import TypeVar as TypeVarExt -from fastmcp.tools.base import ToolResult, resolve_serialize_by_alias +from fastmcp.tools.base import ToolResult from fastmcp.utilities.docstring_parsing import ParsedDocstring, parse_docstring from fastmcp.utilities.json_schema import compress_schema from fastmcp.utilities.logging import get_logger @@ -146,51 +148,30 @@ def _strip_input_required(tp: Any) -> Any: return Union[tuple(residual)] # noqa: UP007 -def _unwrap_model(tp: Any) -> type[BaseModel] | None: - """Unwrap ``Annotated`` and return the underlying Pydantic model, if any.""" - if get_origin(tp) is Annotated: - return _unwrap_model(get_args(tp)[0]) - if isinstance(tp, type) and issubclass(tp, BaseModel): - return tp - return None +class _ToolOutputSchemaGenerator(GenerateJsonSchema): + """Generate each model's schema with its configured serialization aliases. - -def _resolve_output_by_alias(tp: Any) -> bool: - """Resolve ``by_alias`` for the output schema of return type *tp*. - - Unwraps ``Annotated`` and ``Optional``/``Union`` wrappers to find the - underlying Pydantic model so the generated schema honors the model's - ``serialize_by_alias`` config — keeping it consistent with how the runtime - result is serialized. Containers (``list[Model]`` etc.) are not unwrapped: - their schema keeps the default, matching the runtime path which only - special-cases a directly-returned model. - - Known limitation: a single schema is generated with one ``by_alias`` value, - while the runtime resolves the alias mode per returned value. They cannot - diverge for a plain single-model return, but a union return can produce more - than one runtime alias mode that no single schema can describe: - - - distinct models with *conflicting* ``serialize_by_alias`` (e.g. ``A | B`` - where ``A`` opts out but ``B`` opts in), and - - a model arm alongside a container arm (e.g. ``Model | list[Model]``): - a directly-returned model honors its config, but a returned ``list`` is - serialized with the default alias mode, so the two variants disagree. - - Pydantic's schema generator does not consult per-model ``serialize_by_alias`` - and the runtime does not recurse into containers, so honoring every variant - would require per-arm schema assembly. This is an accepted edge; single-model - returns and unions whose arms all resolve to the same mode are consistent. + Pydantic's serializer consults ``serialize_by_alias`` per model, while its + JSON Schema API otherwise applies one ``by_alias`` value to the whole tree. """ - origin = get_origin(tp) - if origin is Annotated: - return _resolve_output_by_alias(get_args(tp)[0]) - if origin is Union or origin is types.UnionType: - for arg in get_args(tp): - model = _unwrap_model(arg) - if model is not None: - return resolve_serialize_by_alias(model) - return True - return resolve_serialize_by_alias(tp) + + def model_schema(self, schema: core_schema.ModelSchema) -> JsonSchemaValue: + previous_by_alias = self.by_alias + configured = schema["cls"].model_config.get("serialize_by_alias") + self.by_alias = False if configured is None else configured + try: + return super().model_schema(schema) + finally: + self.by_alias = previous_by_alias + + def dataclass_schema(self, schema: core_schema.DataclassSchema) -> JsonSchemaValue: + previous_by_alias = self.by_alias + configured = (schema.get("config") or {}).get("serialize_by_alias") + self.by_alias = False if configured is None else configured + try: + return super().dataclass_schema(schema) + finally: + self.by_alias = previous_by_alias T = TypeVarExt("T", default=Any) @@ -449,12 +430,11 @@ class ParsedFunction: ) try: - # Honor the model's serialize_by_alias config so the schema's - # field names match the serialized result (see base.py). - by_alias = _resolve_output_by_alias(clean_output_type) type_adapter = get_cached_typeadapter(clean_output_type) base_schema = type_adapter.json_schema( - mode="serialization", by_alias=by_alias + mode="serialization", + by_alias=False, + schema_generator=_ToolOutputSchemaGenerator, ) # Generate schema for wrapped type if it's non-object @@ -466,7 +446,9 @@ class ParsedFunction: wrapped_type = _WrappedResult[clean_output_type] wrapped_adapter = get_cached_typeadapter(wrapped_type) output_schema = wrapped_adapter.json_schema( - mode="serialization", by_alias=by_alias + mode="serialization", + by_alias=False, + schema_generator=_ToolOutputSchemaGenerator, ) output_schema["x-fastmcp-wrap-result"] = True else: diff --git a/fastmcp_slim/fastmcp/tools/function_tool.py b/fastmcp_slim/fastmcp/tools/function_tool.py index 4204679a8..e5787f466 100644 --- a/fastmcp_slim/fastmcp/tools/function_tool.py +++ b/fastmcp_slim/fastmcp/tools/function_tool.py @@ -197,7 +197,6 @@ def _resolve_param_hints(fn: Callable[..., Any]) -> dict[str, Any]: class FunctionTool(Tool): fn: SkipJsonSchema[Callable[..., Any]] - return_type: Annotated[SkipJsonSchema[Any], Field(exclude=True)] = None run_in_thread: Annotated[ bool, Field( diff --git a/fastmcp_slim/fastmcp/tools/tool_transform.py b/fastmcp_slim/fastmcp/tools/tool_transform.py index 2ac0a2dc3..436f3f2d3 100644 --- a/fastmcp_slim/fastmcp/tools/tool_transform.py +++ b/fastmcp_slim/fastmcp/tools/tool_transform.py @@ -8,7 +8,6 @@ from dataclasses import dataclass from typing import Annotated, Any, Literal, cast import mcp_types -import pydantic_core from mcp_types import ToolAnnotations from pydantic import ConfigDict from pydantic.fields import Field @@ -19,8 +18,6 @@ from fastmcp.tools.base import ( InputRequiredToolResult, Tool, ToolResult, - _convert_to_content, - resolve_serialize_by_alias, ) from fastmcp.tools.function_parsing import ParsedFunction from fastmcp.utilities.async_utils import ( @@ -394,40 +391,7 @@ class TransformedTool(Tool): else: return result - # Otherwise convert to content and create ToolResult with proper structured content - - unstructured_result = _convert_to_content(result) - - structured_output = None - # First handle structured content based on output schema, if any - if self.output_schema is not None: - if self.output_schema.get("x-fastmcp-wrap-result"): - # Schema says wrap - serialize the inner result first (so its - # serialize_by_alias config is honored) before nesting, since - # wrapping in a dict would otherwise mask the model's config. - structured_output = { - "result": pydantic_core.to_jsonable_python( - result, by_alias=resolve_serialize_by_alias(result) - ) - } - else: - structured_output = result - # If no output schema, try to serialize the result. If it is a dict, use - # it as structured content. If it is not a dict, ignore it. - if structured_output is None: - try: - structured_output = pydantic_core.to_jsonable_python( - result, by_alias=resolve_serialize_by_alias(result) - ) - if not isinstance(structured_output, dict): - structured_output = None - except Exception: - pass - - return ToolResult( - content=unstructured_result, - structured_content=structured_output, - ) + return self.convert_result(result) finally: _current_tool.reset(token) @@ -641,6 +605,7 @@ class TransformedTool(Tool): transformed_tool = cls( fn=final_fn, + return_type=parsed_fn.return_type if parsed_fn is not None else None, forwarding_fn=forwarding_fn, parent_tool=tool, name=final_name, diff --git a/tests/tools/tool/test_output_schema.py b/tests/tools/tool/test_output_schema.py index a3b745c4e..c650134d9 100644 --- a/tests/tools/tool/test_output_schema.py +++ b/tests/tools/tool/test_output_schema.py @@ -231,6 +231,10 @@ class TestToolFromFunctionOutputSchema: tool = Tool.from_function(func) assert tool.output_schema is None + result = await tool.run({}) + assert result.structured_content is None + assert len(result.content) == 1 + async def test_mixed_unserializable_return_annotation(self): class Unserializable: def __init__(self, data: Any): diff --git a/tests/tools/tool/test_results.py b/tests/tools/tool/test_results.py index ebe26700b..9edcbb327 100644 --- a/tests/tools/tool/test_results.py +++ b/tests/tools/tool/test_results.py @@ -4,7 +4,7 @@ from typing import Annotated, Any import pytest from mcp_types import CallToolResult, TextContent -from pydantic import BaseModel, ConfigDict, Field +from pydantic import BaseModel, ConfigDict, Field, with_config from fastmcp import Client, FastMCP from fastmcp.tools.base import Tool, ToolResult @@ -200,6 +200,7 @@ class TestSerializationAlias: class Component(BaseModel): """Model with multiple validation aliases but specific serialization alias.""" + model_config = ConfigDict(serialize_by_alias=True) component_id: str = Field( validation_alias=AliasChoices("id", "componentId"), serialization_alias="componentId", @@ -243,6 +244,7 @@ class TestSerializationAlias: class Component(BaseModel): """Model with multiple validation aliases but specific serialization alias.""" + model_config = ConfigDict(serialize_by_alias=True) component_id: str = Field( validation_alias=AliasChoices("id", "componentId"), serialization_alias="componentId", @@ -277,12 +279,7 @@ class TestSerializationAlias: class TestSerializeByAlias: - """Tests that a model's serialize_by_alias config is honored at runtime. - - pydantic_core's serialization helpers default by_alias to True, which - silently ignores serialize_by_alias=False. The serialized result and the - generated output schema must both reflect the model's configured behavior. - """ + """Tests that typed results use Pydantic's serialization behavior.""" async def test_serialize_by_alias_false_uses_field_names(self): """serialize_by_alias=False emits field names in schema, structured, and text.""" @@ -312,8 +309,8 @@ class TestSerializeByAlias: "filepath", } - async def test_unset_config_preserves_alias_default(self): - """A model with an alias but no serialize config keeps emitting the alias.""" + async def test_unset_config_uses_pydantic_default(self): + """A model with no serialize config uses Pydantic's field-name default.""" class Biofile(BaseModel): id: str = Field(alias="_id") @@ -329,14 +326,96 @@ class TestSerializeByAlias: tools = {t.name: t for t in await client.list_tools()} result = await client.call_tool("get_biofile", {}) - assert result.structured_content == {"_id": "123", "filepath": "/p"} + assert result.structured_content == {"id": "123", "filepath": "/p"} assert set(tools["get_biofile"].output_schema["properties"]) == { # type: ignore[index] - "_id", + "id", "filepath", } + async def test_model_in_typed_mapping_respects_config(self): + """A typed mapping's schema and result use the model's field names.""" + + class Biofile(BaseModel): + model_config = ConfigDict(serialize_by_alias=False) + id: str = Field(alias="_id") + + mcp = FastMCP() + + @mcp.tool + def get_biofiles() -> dict[str, Biofile]: + return {"first": Biofile(_id="1")} + + async with Client(mcp) as client: + tools = {tool.name: tool for tool in await client.list_tools()} + result = await client.call_tool("get_biofiles", {}) + + value_schema = tools["get_biofiles"].output_schema["additionalProperties"] # type: ignore[index] + assert set(value_schema["properties"]) == {"id"} + assert result.structured_content == {"first": {"id": "1"}} + + async def test_nested_models_use_their_own_alias_configs(self): + """Nested models can independently enable and disable aliases.""" + + class NamedValue(BaseModel): + model_config = ConfigDict(serialize_by_alias=False) + value: str = Field(serialization_alias="namedValue") + + class AliasedValue(BaseModel): + model_config = ConfigDict(serialize_by_alias=True) + value: str = Field(serialization_alias="aliasedValue") + + class Output(BaseModel): + named: NamedValue + aliased: AliasedValue + + mcp = FastMCP() + + @mcp.tool + def get_output() -> Output: + return Output( + named=NamedValue(value="named"), + aliased=AliasedValue(value="aliased"), + ) + + async with Client(mcp) as client: + tools = {tool.name: tool for tool in await client.list_tools()} + result = await client.call_tool("get_output", {}) + + properties = tools["get_output"].output_schema["properties"] # type: ignore[index] + assert set(properties["named"]["properties"]) == {"value"} + assert set(properties["aliased"]["properties"]) == {"aliasedValue"} + assert result.structured_content == { + "named": {"value": "named"}, + "aliased": {"aliasedValue": "aliased"}, + } + + async def test_typed_dataclass_container_uses_declared_adapter(self): + """A typed container preserves its dataclass's alias configuration.""" + + @with_config(ConfigDict(serialize_by_alias=True)) + @dataclass + class Output: + value: Annotated[str, Field(serialization_alias="dataValue")] + + mcp = FastMCP() + + @mcp.tool + def get_output() -> list[Output]: + return [Output(value="data")] + + async with Client(mcp) as client: + tools = {tool.name: tool for tool in await client.list_tools()} + result = await client.call_tool("get_output", {}) + + item_schema = tools["get_output"].output_schema["properties"]["result"][ # type: ignore[index] + "items" + ] + assert set(item_schema["properties"]) == {"dataValue"} + assert result.structured_content == {"result": [{"dataValue": "data"}]} + assert json.loads(result.content[0].text) == [{"dataValue": "data"}] # type: ignore[union-attr] + async def test_serialize_by_alias_true_uses_alias(self): - """serialize_by_alias=True emits aliases, same as the default.""" + """serialize_by_alias=True emits aliases.""" class Biofile(BaseModel): model_config = ConfigDict(serialize_by_alias=True) @@ -354,80 +433,3 @@ class TestSerializeByAlias: assert result.structured_content == {"_id": "123"} assert set(tools["get_biofile"].output_schema["properties"]) == {"_id"} # type: ignore[index] - - async def test_nested_models_respect_config(self): - """serialize_by_alias=False propagates through nested models.""" - - class Inner(BaseModel): - model_config = ConfigDict(serialize_by_alias=False) - inner_id: str = Field(alias="_iid") - - class Outer(BaseModel): - model_config = ConfigDict(serialize_by_alias=False) - id: str = Field(alias="_id") - inner: Inner - - mcp = FastMCP() - - @mcp.tool - def get_outer() -> Outer: - return Outer(_id="1", inner=Inner(_iid="2")) - - async with Client(mcp) as client: - result = await client.call_tool("get_outer", {}) - - assert result.structured_content == {"id": "1", "inner": {"inner_id": "2"}} - - async def test_annotated_optional_return_stays_consistent(self): - """Annotated[Model, ...] | None resolves the model inside the union arm. - - Regression: the union arm is a typing.Annotated object, so a naive - isinstance check skipped the model and the schema fell back to aliases - while the runtime serialized field names, breaking client validation. - """ - - class Biofile(BaseModel): - model_config = ConfigDict(serialize_by_alias=False) - id: str = Field(alias="_id") - - mcp = FastMCP() - - @mcp.tool - def get_biofile() -> Annotated[Biofile, Field(description="x")] | None: - return Biofile(_id="1") - - async with Client(mcp) as client: - tools = {t.name: t for t in await client.list_tools()} - # client-side validation of structured content against the schema - # raises if they disagree - result = await client.call_tool("get_biofile", {}) - - schema_props = set(tools["get_biofile"].output_schema["properties"]) # type: ignore[index] - assert schema_props == set(result.structured_content) # type: ignore[arg-type] - assert result.structured_content == {"result": {"id": "1"}} - - @pytest.mark.parametrize("serialize_by_alias", [True, False, None]) - async def test_schema_and_structured_content_agree(self, serialize_by_alias): - """The output schema field names always match the structured content keys.""" - if serialize_by_alias is None: - config = ConfigDict() - else: - config = ConfigDict(serialize_by_alias=serialize_by_alias) - - class Model(BaseModel): - model_config = config - id: str = Field(alias="_id") - name: str - - mcp = FastMCP() - - @mcp.tool - def get_model() -> Model: - return Model(_id="1", name="x") - - async with Client(mcp) as client: - tools = {t.name: t for t in await client.list_tools()} - result = await client.call_tool("get_model", {}) - - schema_props = set(tools["get_model"].output_schema["properties"]) # type: ignore[index] - assert schema_props == set(result.structured_content) # type: ignore[arg-type] diff --git a/tests/tools/tool_transform/test_tool_transform.py b/tests/tools/tool_transform/test_tool_transform.py index 2cde40b7f..3dc89239b 100644 --- a/tests/tools/tool_transform/test_tool_transform.py +++ b/tests/tools/tool_transform/test_tool_transform.py @@ -1,11 +1,13 @@ """Core tool transform functionality.""" +import json import re +from dataclasses import dataclass from typing import Annotated, Any import pytest from mcp_types import TextContent -from pydantic import BaseModel, Field +from pydantic import BaseModel, ConfigDict, Field, with_config from fastmcp import FastMCP from fastmcp.client.client import Client @@ -722,6 +724,28 @@ async def test_transform_fn_wrapped_result_respects_serialize_by_alias(): assert result.structured_content == {"result": {"id": "42"}} +async def test_transform_fn_configured_dataclass_respects_serialize_by_alias(): + """A transform uses its return annotation for nested dataclass serialization.""" + + @with_config(ConfigDict(serialize_by_alias=True)) + @dataclass + class Item: + id: Annotated[str, Field(serialization_alias="itemId")] + + def base() -> None: + pass + + async def transform() -> list[Item]: + return [Item(id="42")] + + transformed = Tool.from_tool(base, transform_fn=transform) + result = await transformed.run({}) + + assert result.structured_content == {"result": [{"itemId": "42"}]} + assert isinstance(result.content[0], TextContent) + assert json.loads(result.content[0].text) == [{"itemId": "42"}] + + class TestProxy: @pytest.fixture def mcp_server(self) -> FastMCP: From 1ac8fc6060c2a81a09fdbe04c2d5792c6ad54fd2 Mon Sep 17 00:00:00 2001 From: Chris Guidry <chris.g@prefect.io> Date: Thu, 6 Aug 2026 20:01:40 -0400 Subject: [PATCH 50/53] Encrypt task context snapshots at rest (#4772) Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> --- docs/more/settings.mdx | 2 +- docs/servers/tasks.mdx | 23 + fastmcp_tasks/fastmcp_tasks/context.py | 86 +++- fastmcp_tasks/fastmcp_tasks/encryption.py | 171 +++++++ fastmcp_tasks/fastmcp_tasks/handlers.py | 6 +- fastmcp_tasks/fastmcp_tasks/settings.py | 34 +- fastmcp_tasks/pyproject.toml | 3 + .../tasks/server/test_snapshot_encryption.py | 437 ++++++++++++++++++ tests/tasks/server/test_task_ttl.py | 27 ++ uv.lock | 2 + 10 files changed, 780 insertions(+), 11 deletions(-) create mode 100644 fastmcp_tasks/fastmcp_tasks/encryption.py create mode 100644 tests/tasks/server/test_snapshot_encryption.py diff --git a/docs/more/settings.mdx b/docs/more/settings.mdx index 6ea808af7..e6ea0f584 100644 --- a/docs/more/settings.mdx +++ b/docs/more/settings.mdx @@ -81,7 +81,7 @@ These control how the server listens when running with an HTTP transport. ## Tasks (Docket) -Task settings (the `FASTMCP_DOCKET_` variables) moved to the optional `fastmcp-tasks` package. See [server tasks](/servers/tasks) for configuration. +Task settings (the `FASTMCP_DOCKET_` and `FASTMCP_TASKS_` variables) live in the optional `fastmcp-tasks` package. See [server tasks](/servers/tasks) for configuration, including `FASTMCP_TASKS_ENCRYPTION_KEY` for [encrypting task snapshots at rest](/servers/tasks#credentials-at-rest). ## Security diff --git a/docs/servers/tasks.mdx b/docs/servers/tasks.mdx index b22a7b91d..7b374473d 100644 --- a/docs/servers/tasks.mdx +++ b/docs/servers/tasks.mdx @@ -162,6 +162,7 @@ mcp.add_extension(TasksExtension(url="redis://localhost:6379/0", concurrency=20) | `FASTMCP_DOCKET_URL` | `memory://` | Backend URL (`memory://` or `redis://host:port/db`) | | `FASTMCP_DOCKET_NAME` | `fastmcp` | Queue name. Servers and workers sharing a name and URL share a queue. | | `FASTMCP_DOCKET_CONCURRENCY` | `10` | Maximum concurrent tasks per worker. | +| `FASTMCP_TASKS_ENCRYPTION_KEY` | (unset) | Encrypts [task context snapshots at rest](#credentials-at-rest). Every server and worker sharing a queue must set the same key. | ## Backends @@ -193,6 +194,28 @@ mcp.add_extension(TasksExtension(url="redis://localhost:6379/0")) - **Fast**: Single-digit millisecond task pickup latency - **Scalable**: Add workers to distribute load across processes or machines +### Credentials at Rest + +A background task runs long after the request that submitted it has ended, but it still needs to know who asked for the work. FastMCP captures that identity at submission time in a **task context snapshot**: the caller's access token and every inbound HTTP header, including `Authorization`. The worker restores the snapshot before the tool body runs, so `get_access_token()` and `get_http_headers()` return the submitting caller. + +That snapshot lives in the backend for the task's TTL. With `memory://` it never leaves the process. With Redis or Valkey it is a stored value, and by default it is stored as plaintext JSON. A `rediss://` URL encrypts the connection, not the data the backend holds. Anyone who can read the backend can read the tokens. + +Set `FASTMCP_TASKS_ENCRYPTION_KEY` to encrypt the snapshot before it is written: + +```bash +export FASTMCP_TASKS_ENCRYPTION_KEY=$(python -c "import secrets; print(secrets.token_urlsafe(32))") +``` + +<Warning> +Every server and worker on the same queue must set the same key. The process that restores a snapshot is rarely the one that captured it, and a worker with the wrong key cannot recover the caller. +</Warning> + +With a key configured, restore **fails closed**: a worker that cannot decrypt a snapshot fails the task instead of running the tool with no identity. This matters for a tool whose behavior depends on the caller: running it as an anonymous user is worse than not running it. The failure is reported to the client as a task error, and the server log names the key mismatch. + +Two consequences of failing closed are worth planning for. Tasks submitted before the key was set fail when a worker with the key picks them up, so drain the queue before you roll a key out. Rotating a key does the same to tasks in flight under the old one. + +The key protects the snapshot only. Tool arguments and any answers a task gathers through [mid-task input](#gathering-input-mid-task) are still stored as plaintext, so treat the backend as sensitive regardless. + ## Workers Every FastMCP server with task-enabled tools automatically starts an **embedded worker**. You do not need to start a separate worker process for tasks to execute. diff --git a/fastmcp_tasks/fastmcp_tasks/context.py b/fastmcp_tasks/fastmcp_tasks/context.py index 6a4c8b514..64d6f474b 100644 --- a/fastmcp_tasks/fastmcp_tasks/context.py +++ b/fastmcp_tasks/fastmcp_tasks/context.py @@ -16,6 +16,7 @@ from contextvars import ContextVar from dataclasses import dataclass from typing import TYPE_CHECKING +from fastmcp_tasks.encryption import SnapshotDecryptionError, snapshot_codec from fastmcp_tasks.keys import ( leg_number_from_key, parse_task_key, @@ -133,6 +134,28 @@ def get_task_leg_number() -> int: return 1 +def _snapshot_redis_key(docket: Docket, task_scope: str | None, task_id: str) -> str: + """The Redis key holding a task's context snapshot.""" + return docket.key(f"{task_redis_prefix(task_scope)}:{task_id}:snapshot") + + +async def refresh_snapshot_ttl( + docket: Docket, task_scope: str | None, task_id: str, ttl_seconds: int +) -> None: + """Slide the snapshot key's TTL alongside the task's routing keys. + + An actively polled task refreshes its metadata and leg pointers on every + ``tasks/get``, and the snapshot must live just as long: a re-entered leg + restores the submitting caller from it. Without the refresh, a task parked + on input past the snapshot's creation-time TTL loses the caller, which + means an unauthenticated run without encryption and a failed task with it. + """ + async with docket.redis() as redis: + await redis.expire( + _snapshot_redis_key(docket, task_scope, task_id), ttl_seconds + ) + + @dataclass(frozen=True, slots=True) class TaskContextSnapshot: """All context data snapshotted at task-submission time. @@ -226,10 +249,17 @@ class TaskContextSnapshot: task_id: str, ttl_seconds: int, ) -> None: - """Store this snapshot as a single Redis key.""" - key = docket.key(f"{task_redis_prefix(task_scope)}:{task_id}:snapshot") + """Store this snapshot as a single Redis key. + + The stored value is encrypted when a ``FASTMCP_TASKS_ENCRYPTION_KEY`` is + configured: this payload carries the caller's bearer token and headers, + and a distributed backend keeps it where the backend's operators can + read it (#4747). + """ + key = _snapshot_redis_key(docket, task_scope, task_id) + payload = snapshot_codec().encode(self.to_json()) async with docket.redis() as redis: - await redis.set(key, self.to_json(), ex=ttl_seconds) + await redis.set(key, payload, ex=ttl_seconds) # Cache keyed by task_id so stale entries from previous tasks in the same @@ -285,6 +315,12 @@ async def restore_task_snapshot(key: str = TaskKey()) -> None: backend work transparently (#3897). Failures are non-fatal: the task still runs, and sync helpers return ``None`` as they would have before the snapshot was captured. + + Configuring an encryption key changes that contract. The operator asked for + fail-closed protection, so any failure to retrieve, decrypt, parse, or apply + the snapshot, including a snapshot that is simply missing, escapes this + dependency and fails the task, rather than running the tool without the + submitting caller's identity (#4747). """ try: parts = parse_task_key(key) @@ -295,6 +331,11 @@ async def restore_task_snapshot(key: str = TaskKey()) -> None: from fastmcp.server.dependencies import get_server from fastmcp_tasks.dependencies import _current_docket + # Resolved before anything can fail: a misconfigured key (e.g. an empty + # string) raises here and fails the task, and the branches below read + # `codec.protected` to pick between the fail-open and fail-closed contracts. + codec = snapshot_codec() + try: docket = get_server()._docket except RuntimeError: @@ -302,24 +343,53 @@ async def restore_task_snapshot(key: str = TaskKey()) -> None: if docket is None: docket = _current_docket.get() if docket is None: + if codec.protected: + raise RuntimeError( + "No Docket backend is available to retrieve the protected " + "task snapshot, so the submitting caller cannot be recovered." + ) return task_scope = parts["task_scope"] task_id = parts["client_task_id"] try: async with docket.redis() as redis: - raw = await redis.get( - docket.key(f"{task_redis_prefix(task_scope)}:{task_id}:snapshot") - ) + raw = await redis.get(_snapshot_redis_key(docket, task_scope, task_id)) if raw is None: - return - snapshot = TaskContextSnapshot.from_json(raw) + if not codec.protected: + return + raise RuntimeError( + "The task's context snapshot is missing (its TTL may have " + "expired), so the submitting caller cannot be recovered." + ) + snapshot = TaskContextSnapshot.from_json(codec.decode(raw)) _remember_snapshot(task_id, snapshot) # Restore the ambient request context (auth token, headers) so core's # get_access_token()/get_http_headers() see the submitting caller inside # the worker, exactly as a normal request would. _apply_snapshot_to_context(snapshot) + except SnapshotDecryptionError: + # Docket reports this to the client as a generic dependency-resolution + # failure, so name the cause here. A key mismatch across servers and + # workers is the likely reason and is not guessable from the wire error. + _logger.error( + "Failed to decrypt the task snapshot for %s. Every server and worker " + "on this queue must share the same FASTMCP_TASKS_ENCRYPTION_KEY. The " + "task will fail rather than run without the submitting caller's " + "identity.", + key, + ) + raise except Exception: + if codec.protected: + _logger.error( + "Failed to restore the protected task snapshot for %s. The task " + "will fail rather than run without the submitting caller's " + "identity.", + key, + exc_info=True, + ) + raise _logger.warning("Failed to restore task snapshot for %s", key, exc_info=True) diff --git a/fastmcp_tasks/fastmcp_tasks/encryption.py b/fastmcp_tasks/fastmcp_tasks/encryption.py new file mode 100644 index 000000000..34b4fec12 --- /dev/null +++ b/fastmcp_tasks/fastmcp_tasks/encryption.py @@ -0,0 +1,171 @@ +"""Encryption of the task-context snapshot at rest. + +The snapshot a task carries holds the submitting caller's access token and every +inbound HTTP header, and it lives in the Docket backend for the task's TTL. A +distributed backend therefore keeps bearer credentials in Redis, where a +``rediss://`` URL protects the wire but not the stored value. + +Setting ``FASTMCP_TASKS_ENCRYPTION_KEY`` turns the stored snapshot into a +Fernet token. The same key must reach every server and worker on the queue, +because the process that restores a snapshot is rarely the one that captured it. +""" + +from __future__ import annotations + +from abc import ABC, abstractmethod +from functools import lru_cache +from typing import ClassVar + +from fastmcp.utilities.logging import get_logger +from fastmcp_tasks.settings import tasks_settings + +logger = get_logger(__name__) + +# Domain separation: FASTMCP_TASKS_ENCRYPTION_KEY may protect other task-owned +# state over time, and each use derives its own Fernet key from this material. +_SNAPSHOT_KEY_SALT = "fastmcp-task-snapshot-key" + +# Below this, warn: the keyspace is small enough that the offline attacker this +# feature defends against can search it even through PBKDF2. Matches the OAuth +# proxy's threshold for its signing-key material. +_SHORT_KEY_WARNING_LENGTH = 12 + +# Every Fernet token starts with the version byte 0x80, which base64url encodes +# (together with the leading zero bytes of its 64-bit timestamp) as "gAAAAA". +# A plaintext snapshot is a JSON object starting with "{", so the prefix cannot +# collide with a legitimately unencrypted value. +_FERNET_PREFIX = "gAAAAA" + + +class SnapshotDecryptionError(Exception): + """A stored snapshot is encrypted but cannot be read by this process. + + Raised for a wrong key, a tampered value, a plaintext value written before + the key was configured, or an encrypted value read by a process with no key + configured at all. The restore path lets this escape so the task fails, + rather than running the tool as an anonymous caller. + """ + + +class SnapshotCodec(ABC): + """Transforms snapshot payloads on their way to and from the backend. + + ``protected`` tells the restore path which failure contract applies: a + protected snapshot that cannot be restored fails the task, an unprotected + one degrades to an anonymous run with a warning. + """ + + protected: ClassVar[bool] + + @abstractmethod + def encode(self, payload: str) -> str: + """Return the stored form of a serialized snapshot.""" + + @abstractmethod + def decode(self, stored: str | bytes) -> str: + """Return the serialized snapshot a stored value holds.""" + + +class PlaintextCodec(SnapshotCodec): + """Stores snapshots as-is; the contract when no encryption key is set. + + It still refuses to decode a Fernet envelope: an encrypted snapshot + reaching a keyless process means the submitter configured a key this + process lacks (a partial rollout, or a lost setting), and passing the + ciphertext through would end in a swallowed parse error and an anonymous + run instead of the configured fail-closed behavior. + """ + + protected = False + + def encode(self, payload: str) -> str: + return payload + + def decode(self, stored: str | bytes) -> str: + text = stored.decode() if isinstance(stored, bytes) else stored + if text.startswith(_FERNET_PREFIX): + raise SnapshotDecryptionError( + "The stored task snapshot is encrypted, but this process has " + "no FASTMCP_TASKS_ENCRYPTION_KEY configured." + ) + return text + + +class EncryptedCodec(SnapshotCodec): + """Encrypts snapshot payloads with a key derived from material. + + The material is a string from the environment, and nothing about a string + proves it is random, so it is always treated as low-entropy: the Fernet key + comes from PBKDF2, never from HKDF. The stretch costs about a second, paid + once per process (see ``_codec_for``). + """ + + protected = True + + def __init__(self, material: str) -> None: + from cryptography.fernet import Fernet + + from fastmcp.server.auth.jwt_issuer import derive_jwt_key + + if not material: + raise ValueError( + "FASTMCP_TASKS_ENCRYPTION_KEY must not be empty. Unset it to store " + "task snapshots as plaintext, or set at least 32 random " + "characters." + ) + if len(material) < _SHORT_KEY_WARNING_LENGTH: + logger.warning( + "The configured encryption key is shorter than %d characters; " + "use at least 32 random characters.", + _SHORT_KEY_WARNING_LENGTH, + ) + key = derive_jwt_key(low_entropy_material=material, salt=_SNAPSHOT_KEY_SALT) + + self._fernet = Fernet(key=key) + + def encode(self, payload: str) -> str: + """Return the encrypted form of a serialized snapshot.""" + return self._fernet.encrypt(payload.encode()).decode() + + def decode(self, stored: str | bytes) -> str: + """Return the serialized snapshot a stored value holds. + + Raises ``SnapshotDecryptionError`` if the value was not produced by this + key, including when it is unencrypted. + """ + from cryptography.fernet import InvalidToken + + raw = stored.encode() if isinstance(stored, str) else stored + try: + return self._fernet.decrypt(raw).decode() + except InvalidToken as e: + raise SnapshotDecryptionError( + "The stored task snapshot could not be decrypted with the " + "configured FASTMCP_TASKS_ENCRYPTION_KEY." + ) from e + + +_PLAINTEXT_CODEC = PlaintextCodec() + + +@lru_cache(maxsize=4) +def _codec_for(material: str) -> EncryptedCodec: + """One codec per key, so the derivation cost is paid once per process. + + The PBKDF2 stretch takes about a second, and every task submission and + every restore needs a codec. + """ + return EncryptedCodec(material) + + +def snapshot_codec() -> SnapshotCodec: + """The codec for the configured key; the plaintext codec when none is set.""" + key = tasks_settings.encryption_key + if key is None: + return _PLAINTEXT_CODEC + return _codec_for(key.get_secret_value()) + + +def clear_codec_cache() -> None: + """Drop the cached codecs, so a changed key takes effect.""" + _codec_for.cache_clear() diff --git a/fastmcp_tasks/fastmcp_tasks/handlers.py b/fastmcp_tasks/fastmcp_tasks/handlers.py index 5193d5b84..ae9deb996 100644 --- a/fastmcp_tasks/fastmcp_tasks/handlers.py +++ b/fastmcp_tasks/fastmcp_tasks/handlers.py @@ -32,7 +32,7 @@ from fastmcp.exceptions import NotFoundError from fastmcp.tools.base import InputRequiredToolResult, Tool, ToolResult from fastmcp.utilities.tasks import DEFAULT_POLL_INTERVAL_MS from fastmcp.utilities.versions import VersionSpec -from fastmcp_tasks.context import get_task_scope +from fastmcp_tasks.context import get_task_scope, refresh_snapshot_ttl from fastmcp_tasks.creation import ( TASK_MAPPING_TTL_BUFFER_SECONDS, enqueue_task_leg, @@ -165,6 +165,10 @@ async def _lookup_task( await redis.expire(created_at_key, refresh_ttl) await redis.expire(poll_key, refresh_ttl) await refresh_current_leg_ttl(docket, task_scope, task_id, refresh_ttl) + # The snapshot must outlive the routing keys it serves: a re-entered leg + # restores the submitting caller from it, and with encryption configured a + # missing snapshot fails the task instead of degrading to an anonymous run. + await refresh_snapshot_ttl(docket, task_scope, task_id, refresh_ttl) created_at = created_at_bytes.decode("utf-8") if created_at_bytes else None diff --git a/fastmcp_tasks/fastmcp_tasks/settings.py b/fastmcp_tasks/fastmcp_tasks/settings.py index b836b0f67..8dcc96504 100644 --- a/fastmcp_tasks/fastmcp_tasks/settings.py +++ b/fastmcp_tasks/fastmcp_tasks/settings.py @@ -13,7 +13,7 @@ import os from datetime import timedelta from typing import Annotated -from pydantic import Field +from pydantic import Field, SecretStr from pydantic_settings import BaseSettings, SettingsConfigDict # Load the same dotenv source as core FastMCP settings, so a deployment that @@ -129,6 +129,38 @@ class DocketSettings(BaseSettings): docket_settings = DocketSettings() +class TasksSettings(BaseSettings): + """Settings for the task engine itself, as opposed to its Docket backend.""" + + model_config = SettingsConfigDict( + env_prefix="FASTMCP_TASKS_", + env_file=_ENV_FILE, + extra="ignore", + ) + + encryption_key: Annotated[ + SecretStr | None, + Field( + description=inspect.cleandoc( + """ + Key used to encrypt task context snapshots at rest. The snapshot + carries the submitting caller's access token and HTTP headers, + and it is written to the Docket backend for the task's TTL. + Every server and worker sharing a task queue must set the same + key; a worker that cannot decrypt a snapshot fails the task + rather than running it as an anonymous caller. When unset, the + snapshot is stored as plaintext JSON. The Fernet key is derived + from this value with PBKDF2, so any non-empty string works, but + use at least 32 random characters. + """ + ), + ), + ] = None + + +tasks_settings = TasksSettings() + + class TasksClientSettings(BaseSettings): """Client-side settings for driving background tasks. diff --git a/fastmcp_tasks/pyproject.toml b/fastmcp_tasks/pyproject.toml index a491aaaa8..6a1de018f 100644 --- a/fastmcp_tasks/pyproject.toml +++ b/fastmcp_tasks/pyproject.toml @@ -53,6 +53,9 @@ fallback-version = "0.0.0" [tool.hatch.metadata.hooks.uv-dynamic-versioning] dependencies = [ "fastmcp-slim[server]=={{ version }}", + # Fernet and the PBKDF2 key derivation behind FASTMCP_TASKS_ENCRYPTION_KEY, + # which encrypts task context snapshots at rest. + "cryptography>=43.0.0", "pydocket>=0.20.0", # burner-redis 0.1.7's Windows build crashes the interpreter (native fault, # no Python traceback) running the memory:// backend under pytest-xdist — diff --git a/tests/tasks/server/test_snapshot_encryption.py b/tests/tasks/server/test_snapshot_encryption.py new file mode 100644 index 000000000..0c35b6fe4 --- /dev/null +++ b/tests/tasks/server/test_snapshot_encryption.py @@ -0,0 +1,437 @@ +"""Tests for encryption of the task-context snapshot at rest (#4747). + +The snapshot carries the submitting caller's access token and every inbound HTTP +header, and it is written to the Docket backend for the task's TTL. With a +distributed backend those credentials sit in Redis where the backend's operators +can read them. Setting ``FASTMCP_TASKS_ENCRYPTION_KEY`` makes the snapshot a Fernet +token instead, and makes a worker that cannot decrypt one fail the task rather +than run it as an anonymous caller. +""" + +from __future__ import annotations + +import json +import logging +from collections.abc import Iterator +from unittest.mock import patch + +import pytest +from fastmcp_tasks.context import TaskContextSnapshot +from fastmcp_tasks.encryption import ( + EncryptedCodec, + PlaintextCodec, + SnapshotDecryptionError, + clear_codec_cache, + snapshot_codec, +) +from fastmcp_tasks.keys import task_redis_prefix +from fastmcp_tasks.settings import TasksSettings, tasks_settings +from pydantic import SecretStr + +from fastmcp import FastMCP +from fastmcp.server.dependencies import get_access_token +from fastmcp_tasks import TasksExtension +from tests.tasks.task_helpers import ( + get_task, + make_access_token, + running_task_server, + submit_task, + wait_for_task, +) + +KEY = "a-test-encryption-key-for-snapshots" +OTHER_KEY = "a-different-test-encryption-key-entirely" + + +@pytest.fixture +def encryption_key() -> Iterator[str]: + """Configure the tasks encryption key for the duration of a test.""" + clear_codec_cache() + previous = tasks_settings.encryption_key + tasks_settings.encryption_key = SecretStr(KEY) + try: + yield KEY + finally: + tasks_settings.encryption_key = previous + clear_codec_cache() + + +@pytest.fixture +def no_encryption_key() -> Iterator[None]: + """Guarantee no key is configured, whatever the ambient environment holds.""" + clear_codec_cache() + previous = tasks_settings.encryption_key + tasks_settings.encryption_key = None + try: + yield + finally: + tasks_settings.encryption_key = previous + clear_codec_cache() + + +@pytest.fixture +def sensitive_snapshot() -> TaskContextSnapshot: + """A snapshot carrying a bearer token and an Authorization header.""" + token = make_access_token("client-a", "user-1") + return TaskContextSnapshot( + access_token_json=token.model_dump_json(), + http_headers={"authorization": f"Bearer {token.token}", "x-trace-id": "abc"}, + origin_request_id="req-1", + session_id="session-1", + owning_tool_name="peek", + owning_tool_version="1.0", + ) + + +class TestSnapshotCodec: + def test_round_trips_a_payload(self): + codec = EncryptedCodec(KEY) + assert codec.decode(codec.encode('{"a": 1}')) == '{"a": 1}' + + def test_encoded_payload_hides_the_credentials( + self, sensitive_snapshot: TaskContextSnapshot + ): + encoded = EncryptedCodec(KEY).encode(sensitive_snapshot.to_json()) + assert "token-client-a-user-1" not in encoded + assert "authorization" not in encoded + + def test_decode_rejects_another_keys_payload(self): + encoded = EncryptedCodec(OTHER_KEY).encode('{"a": 1}') + with pytest.raises(SnapshotDecryptionError): + EncryptedCodec(KEY).decode(encoded) + + def test_decode_rejects_plaintext(self): + """A snapshot written before the key was set must not be trusted.""" + with pytest.raises(SnapshotDecryptionError): + EncryptedCodec(KEY).decode('{"access_token_json": null}') + + def test_empty_material_is_rejected(self): + """An empty key would derive a universally reproducible Fernet key.""" + with pytest.raises(ValueError, match="must not be empty"): + EncryptedCodec("") + + def test_decode_accepts_bytes(self): + """Redis hands back bytes on some backends.""" + codec = EncryptedCodec(KEY) + assert codec.decode(codec.encode('{"a": 1}').encode()) == '{"a": 1}' + + def test_same_key_reuses_one_codec(self, encryption_key: str): + assert snapshot_codec() is snapshot_codec() + + def test_plaintext_codec_without_a_key(self, no_encryption_key: None): + codec = snapshot_codec() + assert isinstance(codec, PlaintextCodec) + assert not codec.protected + + def test_plaintext_codec_is_a_pass_through(self): + codec = PlaintextCodec() + assert codec.encode('{"a": 1}') == '{"a": 1}' + assert codec.decode('{"a": 1}') == '{"a": 1}' + assert codec.decode(b'{"a": 1}') == '{"a": 1}' + + def test_plaintext_codec_refuses_an_encrypted_payload(self): + """A keyless process must not pass ciphertext through as plaintext. + + Passing it through would end in a swallowed parse error and an + anonymous run, defeating the submitter's fail-closed configuration. + """ + encrypted = EncryptedCodec(KEY).encode('{"a": 1}') + with pytest.raises( + SnapshotDecryptionError, match="no FASTMCP_TASKS_ENCRYPTION_KEY" + ): + PlaintextCodec().decode(encrypted) + + +class TestTasksSettings: + def test_encryption_key_defaults_to_none(self, monkeypatch: pytest.MonkeyPatch): + monkeypatch.delenv("FASTMCP_TASKS_ENCRYPTION_KEY", raising=False) + + assert TasksSettings().encryption_key is None + + def test_encryption_key_env_var(self, monkeypatch: pytest.MonkeyPatch): + monkeypatch.setenv("FASTMCP_TASKS_ENCRYPTION_KEY", "s3kr1t-material") + + key = TasksSettings().encryption_key + assert key is not None + assert key.get_secret_value() == "s3kr1t-material" + + def test_encryption_key_is_not_printable(self, monkeypatch: pytest.MonkeyPatch): + """A settings dump must never carry the key into a log.""" + monkeypatch.setenv("FASTMCP_TASKS_ENCRYPTION_KEY", "s3kr1t-material") + + assert "s3kr1t-material" not in repr(TasksSettings()) + + +class TestSnapshotSerialization: + def test_json_round_trip_preserves_every_field( + self, sensitive_snapshot: TaskContextSnapshot + ): + assert ( + TaskContextSnapshot.from_json(sensitive_snapshot.to_json()) + == sensitive_snapshot + ) + + +async def _read_stored_snapshot(mcp: FastMCP, task_scope: str, task_id: str) -> str: + """Return the raw stored value of a task's snapshot key.""" + docket = mcp._docket + assert docket is not None + key = docket.key(f"{task_redis_prefix(task_scope)}:{task_id}:snapshot") + async with docket.redis() as redis: + raw = await redis.get(key) + assert raw is not None + return raw.decode() if isinstance(raw, bytes) else str(raw) + + +async def _write_stored_snapshot( + mcp: FastMCP, task_scope: str, task_id: str, payload: str +) -> None: + """Overwrite a task's stored snapshot value.""" + docket = mcp._docket + assert docket is not None + key = docket.key(f"{task_redis_prefix(task_scope)}:{task_id}:snapshot") + async with docket.redis() as redis: + await redis.set(key, payload) + + +async def _delete_stored_snapshot(mcp: FastMCP, task_scope: str, task_id: str) -> None: + """Remove a task's stored snapshot, as a TTL expiry would.""" + docket = mcp._docket + assert docket is not None + key = docket.key(f"{task_redis_prefix(task_scope)}:{task_id}:snapshot") + async with docket.redis() as redis: + await redis.delete(key) + + +@pytest.fixture +def echo_token_server() -> FastMCP: + """A task server whose one tool reports the caller it restored.""" + mcp = FastMCP("snapshot-encryption-test") + mcp.add_extension(TasksExtension()) + + @mcp.tool(task=True) + async def whoami() -> str: + token = get_access_token() + return token.token if token else "no-token" + + return mcp + + +class TestEncryptedSnapshotRoundTrip: + async def test_worker_still_sees_the_submitting_caller( + self, echo_token_server: FastMCP, encryption_key: str + ): + token = make_access_token("client-a", "user-1") + + async with running_task_server(echo_token_server): + created = await submit_task( + echo_token_server, "whoami", {}, access_token=token + ) + final = await wait_for_task( + echo_token_server, created.task_id, access_token=token + ) + + assert final.status == "completed" + assert final.result is not None + assert final.result["structuredContent"] == {"result": token.token} + + async def test_stored_value_is_not_readable( + self, echo_token_server: FastMCP, encryption_key: str + ): + token = make_access_token("client-a", "user-1") + + async with running_task_server(echo_token_server): + created = await submit_task( + echo_token_server, "whoami", {}, access_token=token + ) + stored = await _read_stored_snapshot( + echo_token_server, "client-a|user-1", created.task_id + ) + await wait_for_task(echo_token_server, created.task_id, access_token=token) + + assert token.token not in stored + assert "authorization" not in stored + with pytest.raises(json.JSONDecodeError): + json.loads(stored) + + async def test_undecryptable_snapshot_fails_the_task( + self, + echo_token_server: FastMCP, + encryption_key: str, + caplog: pytest.LogCaptureFixture, + ): + """Fail closed: a worker that cannot recover the caller must not run. + + Running anyway would execute the tool as an anonymous caller, which for + an authorization-sensitive tool is worse than not running at all. Docket + surfaces this on the wire as a generic dependency failure, so the named + cause has to come from the log. + """ + token = make_access_token("client-a", "user-1") + tampered = EncryptedCodec(OTHER_KEY).encode(TaskContextSnapshot().to_json()) + + with caplog.at_level(logging.ERROR, logger="fastmcp_tasks.context"): + async with running_task_server(echo_token_server): + created = await submit_task( + echo_token_server, "whoami", {}, access_token=token + ) + await _write_stored_snapshot( + echo_token_server, "client-a|user-1", created.task_id, tampered + ) + final = await wait_for_task( + echo_token_server, + created.task_id, + access_token=token, + target_states=frozenset({"failed"}), + ) + + assert final.status == "failed" + assert final.error is not None + assert "FASTMCP_TASKS_ENCRYPTION_KEY" in caplog.text + + async def test_missing_snapshot_fails_the_task( + self, echo_token_server: FastMCP, encryption_key: str + ): + """Fail closed extends to a snapshot that is gone, not just unreadable. + + A missing snapshot is reachable in production through TTL expiry, and + it loses the caller just as completely as a wrong key does. + """ + token = make_access_token("client-a", "user-1") + + async with running_task_server(echo_token_server): + created = await submit_task( + echo_token_server, "whoami", {}, access_token=token + ) + await _delete_stored_snapshot( + echo_token_server, "client-a|user-1", created.task_id + ) + final = await wait_for_task( + echo_token_server, + created.task_id, + access_token=token, + target_states=frozenset({"failed"}), + ) + + assert final.status == "failed" + + async def test_unparseable_snapshot_fails_the_task( + self, echo_token_server: FastMCP, encryption_key: str + ): + """Fail closed extends past decryption: a parse failure also loses the + caller, so it must not degrade to an anonymous run.""" + token = make_access_token("client-a", "user-1") + + def boom(*_args, **_kwargs): + raise RuntimeError("simulated deserialization failure") + + async with running_task_server(echo_token_server): + with patch.object(TaskContextSnapshot, "from_json", boom): + created = await submit_task( + echo_token_server, "whoami", {}, access_token=token + ) + final = await wait_for_task( + echo_token_server, + created.task_id, + access_token=token, + target_states=frozenset({"failed"}), + ) + + assert final.status == "failed" + + async def test_keyless_worker_fails_the_encrypted_task( + self, echo_token_server: FastMCP, encryption_key: str + ): + """A worker whose key was lost mid-rollout must not run anonymously. + + The submitter wrote an encrypted snapshot; the restoring process has no + key at all, so its plaintext codec would otherwise pass the ciphertext + through to a parse failure the fail-open path swallows. + """ + token = make_access_token("client-a", "user-1") + + async with running_task_server(echo_token_server): + created = await submit_task( + echo_token_server, "whoami", {}, access_token=token + ) + tasks_settings.encryption_key = None + clear_codec_cache() + final = await wait_for_task( + echo_token_server, + created.task_id, + access_token=token, + target_states=frozenset({"failed"}), + ) + + assert final.status == "failed" + + +class TestUnencryptedByDefault: + async def test_snapshot_stays_plaintext_without_a_key( + self, echo_token_server: FastMCP, no_encryption_key: None + ): + """No key configured is the pre-existing contract, unchanged.""" + token = make_access_token("client-a", "user-1") + + async with running_task_server(echo_token_server): + created = await submit_task( + echo_token_server, "whoami", {}, access_token=token + ) + stored = await _read_stored_snapshot( + echo_token_server, "client-a|user-1", created.task_id + ) + final = await wait_for_task( + echo_token_server, created.task_id, access_token=token + ) + + assert json.loads(stored)["access_token_json"] is not None + assert final.status == "completed" + + async def test_unreadable_snapshot_is_nonfatal_without_a_key( + self, echo_token_server: FastMCP, no_encryption_key: None + ): + """Without encryption a corrupt snapshot still only degrades the caller.""" + token = make_access_token("client-a", "user-1") + + async with running_task_server(echo_token_server): + created = await submit_task( + echo_token_server, "whoami", {}, access_token=token + ) + await _write_stored_snapshot( + echo_token_server, "client-a|user-1", created.task_id, "not json" + ) + final = await wait_for_task( + echo_token_server, created.task_id, access_token=token + ) + + assert final.status == "completed" + assert final.result is not None + assert final.result["structuredContent"] == {"result": "no-token"} + + +class TestTaskStillResolvesAfterFailure: + async def test_failed_task_reports_an_error( + self, echo_token_server: FastMCP, encryption_key: str + ): + """A fail-closed task is still a well-formed `tasks/get` result.""" + token = make_access_token("client-a", "user-1") + tampered = EncryptedCodec(OTHER_KEY).encode(TaskContextSnapshot().to_json()) + + async with running_task_server(echo_token_server): + created = await submit_task( + echo_token_server, "whoami", {}, access_token=token + ) + await _write_stored_snapshot( + echo_token_server, "client-a|user-1", created.task_id, tampered + ) + await wait_for_task( + echo_token_server, + created.task_id, + access_token=token, + target_states=frozenset({"failed"}), + ) + fetched = await get_task( + echo_token_server, created.task_id, access_token=token + ) + + assert fetched.status == "failed" diff --git a/tests/tasks/server/test_task_ttl.py b/tests/tasks/server/test_task_ttl.py index 8fa9f670e..edffda25a 100644 --- a/tests/tasks/server/test_task_ttl.py +++ b/tests/tasks/server/test_task_ttl.py @@ -96,3 +96,30 @@ async def test_poll_refreshes_routing_key_ttl(): async with docket.redis() as redis: # Refreshed well past the shrunk 5s, back toward the full window. assert await redis.ttl(key) > 60 + + +async def test_poll_refreshes_snapshot_ttl(): + """A poll extends the context snapshot's TTL alongside the routing keys. + + A re-entered leg restores the submitting caller from the snapshot, so an + actively polled task must never outlive it: without encryption an expired + snapshot degrades the leg to an anonymous run, and with encryption it fails + the task. After shrinking the snapshot's TTL, a `tasks/get` restores it. + """ + from fastmcp_tasks.context import _snapshot_redis_key + + mcp = _ttl_server() + async with running_task_server(mcp): + created = await submit_task(mcp, "slow_task", {}) + docket = mcp._docket + assert docket is not None + key = _snapshot_redis_key(docket, None, created.task_id) + + async with docket.redis() as redis: + await redis.expire(key, 5) + assert await redis.ttl(key) <= 5 + + await get_task(mcp, created.task_id) + + async with docket.redis() as redis: + assert await redis.ttl(key) > 60 diff --git a/uv.lock b/uv.lock index 58fe0e134..a3d4ce109 100644 --- a/uv.lock +++ b/uv.lock @@ -1089,6 +1089,7 @@ name = "fastmcp-tasks" source = { editable = "fastmcp_tasks" } dependencies = [ { name = "burner-redis", marker = "sys_platform == 'win32'" }, + { name = "cryptography" }, { name = "fastmcp-slim", extra = ["server"] }, { name = "pydocket" }, ] @@ -1096,6 +1097,7 @@ dependencies = [ [package.metadata] requires-dist = [ { name = "burner-redis", marker = "sys_platform == 'win32'", specifier = "<0.1.7" }, + { name = "cryptography", specifier = ">=43.0.0" }, { name = "fastmcp-slim", extras = ["server"], editable = "fastmcp_slim" }, { name = "pydocket", specifier = ">=0.20.0" }, ] From 706f7d269534f55696afcf26d907e4afea258b0d Mon Sep 17 00:00:00 2001 From: Jamie Zieziula <jamie@prefect.io> Date: Thu, 6 Aug 2026 20:02:29 -0400 Subject: [PATCH 51/53] feat(renovate): migrate to Renovate, retire Dependabot (#4754) Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --- .github/dependabot.yml | 14 -------------- renovate.json | 7 +++++++ 2 files changed, 7 insertions(+), 14 deletions(-) delete mode 100644 .github/dependabot.yml create mode 100644 renovate.json diff --git a/.github/dependabot.yml b/.github/dependabot.yml deleted file mode 100644 index 20d3ccecf..000000000 --- a/.github/dependabot.yml +++ /dev/null @@ -1,14 +0,0 @@ -version: 2 -updates: - - package-ecosystem: "pip" - directory: "/" - schedule: - interval: "daily" - labels: - - "dependencies" - - package-ecosystem: "github-actions" - directory: "/" - schedule: - interval: "weekly" - labels: - - "dependencies" diff --git a/renovate.json b/renovate.json new file mode 100644 index 000000000..c6f1da245 --- /dev/null +++ b/renovate.json @@ -0,0 +1,7 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": [ + "github>PrefectHQ/renovate-config", + "github>PrefectHQ/renovate-config:python" + ] +} From 06fee6d30062adf7c1676a608d71fea4f79c0385 Mon Sep 17 00:00:00 2001 From: Sai Mouli <141447420+SaiMouli3@users.noreply.github.com> Date: Fri, 7 Aug 2026 05:39:36 +0530 Subject: [PATCH 52/53] Serialize the event store's stream list read-modify-write (#4758) Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> --- fastmcp_slim/fastmcp/server/event_store.py | 51 +++++++++---- tests/server/test_event_store.py | 84 ++++++++++++++++++++++ 2 files changed, 121 insertions(+), 14 deletions(-) diff --git a/fastmcp_slim/fastmcp/server/event_store.py b/fastmcp_slim/fastmcp/server/event_store.py index bdc504865..a7efc4fdc 100644 --- a/fastmcp_slim/fastmcp/server/event_store.py +++ b/fastmcp_slim/fastmcp/server/event_store.py @@ -8,6 +8,7 @@ AsyncKeyValue protocol, allowing users to configure any compatible backend from __future__ import annotations +import asyncio from uuid import uuid4 from key_value.aio.adapters.pydantic import PydanticAdapter @@ -30,6 +31,9 @@ logger = get_logger(__name__) # TypeAdapter to validate a stored dict back into the correct member. _jsonrpc_message_adapter: TypeAdapter[JSONRPCMessage] = TypeAdapter(JSONRPCMessage) +# Number of striped locks guarding stream event lists. See EventStore.__init__. +_LOCK_STRIPES = 64 + class EventEntry(FastMCPBaseModel): """Stored event entry.""" @@ -84,6 +88,20 @@ class EventStore(SDKEventStore): self._storage: AsyncKeyValue = storage or MemoryStore() self._max_events_per_stream = max_events_per_stream self._ttl = ttl + # Serializes the read-modify-write of each stream's event list. A fixed + # set of striped locks rather than one lock per stream: a single store is + # shared by every session, so a store-wide lock would serialize unrelated + # streams across a Redis round-trip, while a per-stream map would grow + # with every session and need its own eviction. Two streams only contend + # when their IDs collide on the same stripe. + # + # In-process locks are enough because a stream list only ever has + # in-process writers: every transport gets its own SessionScopedEventStore + # with a random per-session prefix, so no two servers sharing one backend + # address the same stream key. Coordinating across processes would need a + # compare-and-swap or transactional update, which AsyncKeyValue does not + # expose -- it offers only get/put/delete/ttl. + self._stream_locks = tuple(asyncio.Lock() for _ in range(_LOCK_STRIPES)) # PydanticAdapter for type-safe storage (following OAuth proxy pattern) self._event_store: PydanticAdapter[EventEntry] = PydanticAdapter[EventEntry]( @@ -121,22 +139,27 @@ class EventStore(SDKEventStore): ) await self._event_store.put(key=event_id, value=entry, ttl=self._ttl) - # Update stream's event list - stream_data = await self._stream_store.get(key=stream_id) - event_ids = stream_data.event_ids if stream_data else [] - event_ids.append(event_id) + # Update stream's event list. A session stores events from more than one + # task -- the SSE writer and the message router both do -- so this + # read-modify-write has to be serialized. Interleaved, each task reads the + # same list, appends only its own ID, and the later write drops the other + # event entirely while both tasks evict the same expired IDs. + async with self._stream_locks[hash(stream_id) % _LOCK_STRIPES]: + stream_data = await self._stream_store.get(key=stream_id) + event_ids = stream_data.event_ids if stream_data else [] + event_ids.append(event_id) - # Trim to max events (delete old events) - if len(event_ids) > self._max_events_per_stream: - for old_id in event_ids[: -self._max_events_per_stream]: - await self._event_store.delete(key=old_id) - event_ids = event_ids[-self._max_events_per_stream :] + # Trim to max events (delete old events) + if len(event_ids) > self._max_events_per_stream: + for old_id in event_ids[: -self._max_events_per_stream]: + await self._event_store.delete(key=old_id) + event_ids = event_ids[-self._max_events_per_stream :] - await self._stream_store.put( - key=stream_id, - value=StreamEventList(event_ids=event_ids), - ttl=self._ttl, - ) + await self._stream_store.put( + key=stream_id, + value=StreamEventList(event_ids=event_ids), + ttl=self._ttl, + ) return event_id diff --git a/tests/server/test_event_store.py b/tests/server/test_event_store.py index edb00b5e8..8ff4203f4 100644 --- a/tests/server/test_event_store.py +++ b/tests/server/test_event_store.py @@ -1,10 +1,13 @@ """Tests for the EventStore implementation.""" +import asyncio + import pytest from mcp.server.streamable_http import EventMessage from mcp_types import JSONRPCRequest from fastmcp.server.event_store import ( + _LOCK_STRIPES, EventEntry, EventStore, SessionScopedEventStore, @@ -260,6 +263,87 @@ class TestEventStore: assert len(replayed) == 1 +class TestConcurrentStoreEvent: + async def test_concurrent_stores_on_one_stream(self, monkeypatch): + """Concurrent stores must not lose events or evict the same ID twice. + + A live session stores events from more than one task (the SSE writer and + the message router), so the stream's event list is read and written + concurrently. Interleaved, each task appends only its own ID to the list + it read, and both evict the same expired IDs -- the second delete is the + one that raised `FileNotFoundError` on a file-backed store. + """ + event_store = EventStore(max_events_per_stream=2) + + stream_get = event_store._stream_store.get + event_delete = event_store._event_store.delete + deleted: list[str] = [] + + async def yielding_get(**kwargs): + # Suspend between the read and the write so the tasks interleave. + stream_data = await stream_get(**kwargs) + await asyncio.sleep(0) + return stream_data + + async def recording_delete(**kwargs): + deleted.append(kwargs["key"]) + return await event_delete(**kwargs) + + monkeypatch.setattr(event_store._stream_store, "get", yielding_get) + monkeypatch.setattr(event_store._event_store, "delete", recording_delete) + + message = JSONRPCRequest(jsonrpc="2.0", method="test", id=1) + event_ids = await asyncio.gather( + *(event_store.store_event("stream-1", message) for _ in range(5)) + ) + + stream_data = await stream_get(key="stream-1") + assert stream_data is not None + # The two most recent events are retained; every other ID was evicted + # exactly once, and no ID vanished without being evicted. + assert len(stream_data.event_ids) == 2 + assert sorted(stream_data.event_ids + deleted) == sorted(event_ids) + assert len(deleted) == len(set(deleted)) + + async def test_distinct_streams_are_not_serialized(self, monkeypatch): + """Unrelated streams must not wait on each other's backend calls. + + One EventStore is shared by every session, so a store-wide lock would + put a Redis round-trip for one session in front of every other one. + """ + event_store = EventStore() + + # hash() is salted per process, so pick the second stream at runtime. + first = "stream-a" + second = next( + candidate + for candidate in (f"stream-{i}" for i in range(1000)) + if hash(candidate) % _LOCK_STRIPES != hash(first) % _LOCK_STRIPES + ) + + stream_get = event_store._stream_store.get + both_inside = asyncio.Event() + inside = 0 + + async def gate(**kwargs): + nonlocal inside + inside += 1 + if inside == 2: + both_inside.set() + # Both critical sections have to be open at once; a store-wide lock + # would keep the second task out until the first finished. + await asyncio.wait_for(both_inside.wait(), timeout=2) + return await stream_get(**kwargs) + + monkeypatch.setattr(event_store._stream_store, "get", gate) + + message = JSONRPCRequest(jsonrpc="2.0", method="test", id=1) + await asyncio.gather( + event_store.store_event(first, message), + event_store.store_event(second, message), + ) + + class TestEventStoreIntegration: """Integration tests for EventStore with actual message types.""" From 8a1820f1c38401fa02c8996a7a30e864561b80d3 Mon Sep 17 00:00:00 2001 From: "marvin-context-protocol[bot]" <225465937+marvin-context-protocol[bot]@users.noreply.github.com> Date: Fri, 7 Aug 2026 15:49:36 -0500 Subject: [PATCH 53/53] chore: Update SDK documentation (#4782) Co-authored-by: marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com> --- docs/python-sdk/fastmcp-server-event_store.mdx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/python-sdk/fastmcp-server-event_store.mdx b/docs/python-sdk/fastmcp-server-event_store.mdx index 08d266ea5..39a2ba77b 100644 --- a/docs/python-sdk/fastmcp-server-event_store.mdx +++ b/docs/python-sdk/fastmcp-server-event_store.mdx @@ -16,19 +16,19 @@ AsyncKeyValue protocol, allowing users to configure any compatible backend ## Classes -### `EventEntry` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/event_store.py#L34" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> +### `EventEntry` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/event_store.py#L38" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> Stored event entry. -### `StreamEventList` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/event_store.py#L42" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> +### `StreamEventList` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/event_store.py#L46" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> List of event IDs for a stream. -### `EventStore` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/event_store.py#L48" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> +### `EventStore` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/event_store.py#L52" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> EventStore implementation backed by AsyncKeyValue. @@ -45,7 +45,7 @@ following the same pattern as ResponseCachingMiddleware and OAuthProxy. **Methods:** -#### `store_event` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/event_store.py#L102" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> +#### `store_event` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/event_store.py#L120" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> ```python store_event(self, stream_id: StreamId, message: JSONRPCMessage | None) -> EventId @@ -61,7 +61,7 @@ Store an event and return its ID. - The generated event ID for the stored event -#### `replay_events_after` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/event_store.py#L143" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> +#### `replay_events_after` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/event_store.py#L166" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup> ```python replay_events_after(self, last_event_id: EventId, send_callback: EventCallback) -> StreamId | None