From 5dd8cde2f9e5d9423fbaf2e6eb2e500693399cc4 Mon Sep 17 00:00:00 2001
From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>
Date: Mon, 16 Feb 2026 16:43:15 -0500
Subject: [PATCH] Drop diskcache dependency (CVE-2025-69872) (#3185)
* Drop diskcache dependency (CVE-2025-69872)
Switch default OAuth proxy storage from DiskStore (backed by diskcache,
which uses pickle serialization) to FileTreeStore (file-per-key JSON).
This removes diskcache from the dependency tree entirely, resolving
CVE-2025-69872 for pip-audit and similar scanners.
* chore: Update SDK documentation
* Add comments explaining FileTreeStore warning suppression
* chore: Update SDK documentation
* Isolate storage by encryption key, gracefully handle decryption failures
* chore: Update SDK documentation
* Bump py-key-value-aio lower bound to 0.4.2 for FileTreeStore security hardening
* chore: Update SDK documentation
* Document storage backend change and update DiskStore references
* Bump py-key-value-aio lower bound to 0.4.3
* Bump py-key-value-aio to 0.4.4, remove warning suppression
* chore: Update SDK documentation
---------
Co-authored-by: marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com>
---
docs/development/upgrade-guide.mdx | 32 ++++++++
.../fastmcp-server-auth-oauth_proxy-proxy.mdx | 26 +++----
docs/servers/storage-backends.mdx | 75 ++++++++++++-------
pyproject.toml | 2 +-
src/fastmcp/server/auth/oauth_proxy/proxy.py | 34 +++++++--
src/fastmcp/server/auth/oidc_proxy.py | 4 +-
src/fastmcp/server/auth/providers/auth0.py | 4 +-
src/fastmcp/server/auth/providers/aws.py | 4 +-
src/fastmcp/server/auth/providers/azure.py | 4 +-
src/fastmcp/server/auth/providers/discord.py | 4 +-
src/fastmcp/server/auth/providers/github.py | 4 +-
src/fastmcp/server/auth/providers/google.py | 4 +-
src/fastmcp/server/auth/providers/workos.py | 4 +-
tests/conftest.py | 4 +-
tests/server/auth/test_oauth_proxy_storage.py | 13 ++--
tests/server/middleware/test_caching.py | 28 +++++--
uv.lock | 69 ++++++++++-------
17 files changed, 209 insertions(+), 106 deletions(-)
diff --git a/docs/development/upgrade-guide.mdx b/docs/development/upgrade-guide.mdx
index e20f2c76c..d67e20123 100644
--- a/docs/development/upgrade-guide.mdx
+++ b/docs/development/upgrade-guide.mdx
@@ -14,6 +14,38 @@ Most servers need only one change: update your import from `from mcp.server.fast
### Breaking Changes
+#### OAuth Storage Backend Changed (diskcache CVE)
+
+The default OAuth storage has moved from `DiskStore` to `FileTreeStore` to address a pickle deserialization vulnerability in diskcache ([CVE-2025-69872](https://github.com/jlowin/fastmcp/issues/3166)).
+
+If you were using the default storage (i.e., not passing an explicit `client_storage`), clients will need to re-register on their first connection after upgrading. This happens automatically — no user action required, and it's the same flow that already occurs whenever a server restarts with in-memory storage. No code changes needed.
+
+If you were passing a `DiskStore` explicitly, you have two options:
+
+1. **Keep using DiskStore** by adding the dependency yourself. This re-introduces the vulnerable `diskcache` package into your dependency tree:
+
+```bash
+pip install 'py-key-value-aio[disk]'
+```
+
+2. **Switch to FileTreeStore** (recommended) or any other [storage backend](/servers/storage-backends):
+
+```python
+from pathlib import Path
+from key_value.aio.stores.filetree import (
+ FileTreeStore,
+ FileTreeV1KeySanitizationStrategy,
+ FileTreeV1CollectionSanitizationStrategy,
+)
+
+storage_dir = Path("/var/lib/fastmcp/oauth")
+store = FileTreeStore(
+ data_directory=storage_dir,
+ key_sanitization_strategy=FileTreeV1KeySanitizationStrategy(storage_dir),
+ collection_sanitization_strategy=FileTreeV1CollectionSanitizationStrategy(storage_dir),
+)
+```
+
#### WSTransport Removed
Use `StreamableHttpTransport` instead.
diff --git a/docs/python-sdk/fastmcp-server-auth-oauth_proxy-proxy.mdx b/docs/python-sdk/fastmcp-server-auth-oauth_proxy-proxy.mdx
index b89d05edc..d7f692aec 100644
--- a/docs/python-sdk/fastmcp-server-auth-oauth_proxy-proxy.mdx
+++ b/docs/python-sdk/fastmcp-server-auth-oauth_proxy-proxy.mdx
@@ -26,7 +26,7 @@ production use with enterprise identity providers.
## Classes
-### `OAuthProxy`
+### `OAuthProxy`
OAuth provider that presents a DCR-compliant interface while proxying to non-DCR IDPs.
@@ -140,7 +140,7 @@ Handles provider-specific requirements:
**Methods:**
-#### `set_mcp_path`
+#### `set_mcp_path`
```python
set_mcp_path(self, mcp_path: str | None) -> None
@@ -157,7 +157,7 @@ this specific MCP endpoint.
- `mcp_path`: The path where the MCP endpoint is mounted (e.g., "/mcp")
-#### `jwt_issuer`
+#### `jwt_issuer`
```python
jwt_issuer(self) -> JWTIssuer
@@ -169,7 +169,7 @@ The JWT issuer is created when set_mcp_path() is called (via get_routes()).
This property ensures a clear error if used before initialization.
-#### `get_client`
+#### `get_client`
```python
get_client(self, client_id: str) -> OAuthClientInformationFull | None
@@ -182,7 +182,7 @@ For unregistered clients, returns None (which will raise an error in the SDK).
CIMD clients (URL-based client IDs) are looked up and cached automatically.
-#### `register_client`
+#### `register_client`
```python
register_client(self, client_info: OAuthClientInformationFull) -> None
@@ -196,7 +196,7 @@ redirect URI will likely be localhost or unknown to the proxied IDP. The
proxied IDP only knows about this server's fixed redirect URI.
-#### `authorize`
+#### `authorize`
```python
authorize(self, client: OAuthClientInformationFull, params: AuthorizationParams) -> str
@@ -214,7 +214,7 @@ If consent is disabled (require_authorization_consent=False), skip the consent s
and redirect directly to the upstream IdP.
-#### `load_authorization_code`
+#### `load_authorization_code`
```python
load_authorization_code(self, client: OAuthClientInformationFull, authorization_code: str) -> AuthorizationCode | None
@@ -226,7 +226,7 @@ Look up our client code and return authorization code object
with PKCE challenge for validation.
-#### `exchange_authorization_code`
+#### `exchange_authorization_code`
```python
exchange_authorization_code(self, client: OAuthClientInformationFull, authorization_code: AuthorizationCode) -> OAuthToken
@@ -244,7 +244,7 @@ Implements the token factory pattern:
PKCE validation is handled by the MCP framework before this method is called.
-#### `load_refresh_token`
+#### `load_refresh_token`
```python
load_refresh_token(self, client: OAuthClientInformationFull, refresh_token: str) -> RefreshToken | None
@@ -256,7 +256,7 @@ Looks up by token hash and reconstructs the RefreshToken object.
Validates that the token belongs to the requesting client.
-#### `exchange_refresh_token`
+#### `exchange_refresh_token`
```python
exchange_refresh_token(self, client: OAuthClientInformationFull, refresh_token: RefreshToken, scopes: list[str]) -> OAuthToken
@@ -273,7 +273,7 @@ Implements two-tier refresh:
6. Keep same FastMCP refresh token (unless upstream rotates)
-#### `load_access_token`
+#### `load_access_token`
```python
load_access_token(self, token: str) -> AccessToken | None
@@ -292,7 +292,7 @@ The FastMCP JWT is a reference token - all authorization data comes
from validating the upstream token via the TokenVerifier.
-#### `revoke_token`
+#### `revoke_token`
```python
revoke_token(self, token: AccessToken | RefreshToken) -> None
@@ -305,7 +305,7 @@ For all tokens, attempts upstream revocation if endpoint is configured.
Access token JTI mappings expire via TTL.
-#### `get_routes`
+#### `get_routes`
```python
get_routes(self, mcp_path: str | None = None) -> list[Route]
diff --git a/docs/servers/storage-backends.mdx b/docs/servers/storage-backends.mdx
index f60ea1821..1bdb19b20 100644
--- a/docs/servers/storage-backends.mdx
+++ b/docs/servers/storage-backends.mdx
@@ -38,39 +38,38 @@ cache_store = MemoryStore()
- ❌ Data lost on restart
- ❌ Not suitable for multi-process deployments
-### Disk Storage
+### File Storage
**Best for:** Single-server production deployments, persistent caching
-Disk storage persists data to the filesystem, allowing it to survive server restarts.
+File storage persists data to the filesystem as one JSON file per key, allowing it to survive server restarts. This is the default backend for OAuth storage on Mac and Windows.
```python
-from key_value.aio.stores.disk import DiskStore
+from pathlib import Path
+from key_value.aio.stores.filetree import (
+ FileTreeStore,
+ FileTreeV1KeySanitizationStrategy,
+ FileTreeV1CollectionSanitizationStrategy,
+)
from fastmcp.server.middleware.caching import ResponseCachingMiddleware
+storage_dir = Path("/var/cache/fastmcp")
+store = FileTreeStore(
+ data_directory=storage_dir,
+ key_sanitization_strategy=FileTreeV1KeySanitizationStrategy(storage_dir),
+ collection_sanitization_strategy=FileTreeV1CollectionSanitizationStrategy(storage_dir),
+)
+
# Persistent response cache
-middleware = ResponseCachingMiddleware(
- cache_storage=DiskStore(directory="/var/cache/fastmcp")
-)
+middleware = ResponseCachingMiddleware(cache_storage=store)
```
-Or with OAuth token storage:
-
-```python
-from fastmcp.server.auth.providers.github import GitHubProvider
-from key_value.aio.stores.disk import DiskStore
-
-auth = GitHubProvider(
- client_id="your-id",
- client_secret="your-secret",
- base_url="https://your-server.com",
- client_storage=DiskStore(directory="/var/lib/fastmcp/oauth")
-)
-```
+The sanitization strategies ensure keys and collection names are safe for the filesystem — alphanumeric names pass through as-is for readability, while special characters are hashed to prevent path traversal.
**Characteristics:**
- ✅ Data persists across restarts
-- ✅ Good performance for moderate load
+- ✅ No external dependencies
+- ✅ Human-readable files on disk
- ❌ Not suitable for distributed deployments
- ❌ Filesystem access required
@@ -203,16 +202,26 @@ Both parameters are required for production. **Wrap your storage in `FernetEncry
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:
```python
+from pathlib import Path
from fastmcp import FastMCP
from fastmcp.server.middleware.caching import ResponseCachingMiddleware
-from key_value.aio.stores.disk import DiskStore
+from key_value.aio.stores.filetree import (
+ FileTreeStore,
+ FileTreeV1KeySanitizationStrategy,
+ FileTreeV1CollectionSanitizationStrategy,
+)
mcp = FastMCP("My Server")
+cache_dir = Path("cache")
+cache_store = FileTreeStore(
+ data_directory=cache_dir,
+ key_sanitization_strategy=FileTreeV1KeySanitizationStrategy(cache_dir),
+ collection_sanitization_strategy=FileTreeV1CollectionSanitizationStrategy(cache_dir),
+)
+
# Cache to disk instead of memory
-mcp.add_middleware(ResponseCachingMiddleware(
- cache_storage=DiskStore(directory="cache")
-))
+mcp.add_middleware(ResponseCachingMiddleware(cache_storage=cache_store))
```
For multi-server deployments sharing a Redis instance:
@@ -236,11 +245,21 @@ middleware = ResponseCachingMiddleware(cache_storage=namespaced_store)
The [FastMCP Client](/clients/client) uses storage for persisting OAuth tokens locally. By default, tokens are stored in memory:
```python
+from pathlib import Path
from fastmcp.client.auth import OAuthClientProvider
-from key_value.aio.stores.disk import DiskStore
+from key_value.aio.stores.filetree import (
+ FileTreeStore,
+ FileTreeV1KeySanitizationStrategy,
+ FileTreeV1CollectionSanitizationStrategy,
+)
# Store tokens on disk for persistence across restarts
-token_storage = DiskStore(directory="~/.local/share/fastmcp/tokens")
+token_dir = Path("~/.local/share/fastmcp/tokens").expanduser()
+token_storage = FileTreeStore(
+ data_directory=token_dir,
+ key_sanitization_strategy=FileTreeV1KeySanitizationStrategy(token_dir),
+ collection_sanitization_strategy=FileTreeV1CollectionSanitizationStrategy(token_dir),
+)
oauth_provider = OAuthClientProvider(
mcp_url="https://your-mcp-server.com/mcp/sse",
@@ -255,7 +274,7 @@ This allows clients to reconnect without re-authenticating after restarts.
| Backend | Development | Single Server | Multi-Server | Cloud Native |
|---------|-------------|---------------|--------------|--------------|
| Memory | ✅ Best | ⚠️ Limited | ❌ | ❌ |
-| Disk | ✅ Good | ✅ Recommended | ❌ | ⚠️ |
+| File | ✅ Good | ✅ Recommended | ❌ | ⚠️ |
| Redis | ⚠️ Overkill | ✅ Good | ✅ Best | ✅ Best |
| DynamoDB | ❌ | ⚠️ | ✅ | ✅ Best (AWS) |
| MongoDB | ❌ | ⚠️ | ✅ | ✅ Good |
@@ -263,7 +282,7 @@ This allows clients to reconnect without re-authenticating after restarts.
**Decision tree:**
1. **Just starting?** Use **Memory** (default) - no configuration needed
-2. **Single server, needs persistence?** Use **Disk**
+2. **Single server, needs persistence?** Use **File**
3. **Multiple servers or cloud deployment?** Use **Redis** or **DynamoDB**
4. **Existing infrastructure?** Look for a matching py-key-value-aio backend
diff --git a/pyproject.toml b/pyproject.toml
index 57954634e..059eafb05 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -18,7 +18,7 @@ dependencies = [
"pydantic[email]>=2.11.7",
"pyyaml>=6.0,<7.0",
"pyperclip>=1.9.0",
- "py-key-value-aio[disk,keyring,memory]>=0.4.0,<0.5.0",
+ "py-key-value-aio[filetree,keyring,memory]>=0.4.4,<0.5.0",
"uvicorn>=0.35",
"websockets>=15.0.1",
"jsonschema-path>=0.3.4",
diff --git a/src/fastmcp/server/auth/oauth_proxy/proxy.py b/src/fastmcp/server/auth/oauth_proxy/proxy.py
index 27773ef25..927e4f1db 100644
--- a/src/fastmcp/server/auth/oauth_proxy/proxy.py
+++ b/src/fastmcp/server/auth/oauth_proxy/proxy.py
@@ -31,6 +31,11 @@ from authlib.integrations.httpx_client import AsyncOAuth2Client
from cryptography.fernet import Fernet
from key_value.aio.adapters.pydantic import PydanticAdapter
from key_value.aio.protocols import AsyncKeyValue
+from key_value.aio.stores.filetree import (
+ FileTreeStore,
+ FileTreeV1CollectionSanitizationStrategy,
+ FileTreeV1KeySanitizationStrategy,
+)
from key_value.aio.wrappers.encryption import FernetEncryptionWrapper
from mcp.server.auth.handlers.metadata import MetadataHandler
from mcp.server.auth.provider import (
@@ -292,7 +297,7 @@ class OAuthProxy(OAuthProvider, ConsentMixin):
extra_token_params: Additional parameters to forward to the upstream token endpoint.
Useful for provider-specific parameters during token exchange.
client_storage: Storage backend for OAuth state (client registrations, tokens).
- If None, an encrypted DiskStore will be created in the data directory.
+ 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).
@@ -413,18 +418,35 @@ class OAuthProxy(OAuthProvider, ConsentMixin):
# JWTIssuer will be created in set_mcp_path() with correct audience
self._jwt_issuer: JWTIssuer | None = None
- # If the user does not provide a store, we will provide an encrypted disk store
+ # If the user does not provide a store, we will provide an encrypted file store.
+ # The storage directory is derived from the encryption key so that different
+ # keys get isolated directories (e.g. two servers on the same machine with
+ # different keys won't collide). Decryption errors are treated as cache misses
+ # rather than hard failures, so key rotation just causes re-registration.
if client_storage is None:
- # Import lazily to avoid sqlite3 dependency when not using OAuthProxy
- from key_value.aio.stores.disk import DiskStore
-
storage_encryption_key = derive_jwt_key(
high_entropy_material=jwt_signing_key.decode(),
salt="fastmcp-storage-encryption-key",
)
+
+ key_fingerprint = hashlib.sha256(storage_encryption_key).hexdigest()[:12]
+ storage_dir = settings.home / "oauth-proxy" / key_fingerprint
+ storage_dir.mkdir(parents=True, exist_ok=True)
+
+ file_store = FileTreeStore(
+ data_directory=storage_dir,
+ key_sanitization_strategy=FileTreeV1KeySanitizationStrategy(
+ storage_dir
+ ),
+ collection_sanitization_strategy=FileTreeV1CollectionSanitizationStrategy(
+ storage_dir
+ ),
+ )
+
client_storage = FernetEncryptionWrapper(
- key_value=DiskStore(directory=settings.home / "oauth-proxy"),
+ key_value=file_store,
fernet=Fernet(key=storage_encryption_key),
+ raise_on_decryption_error=False,
)
self._client_storage: AsyncKeyValue = client_storage
diff --git a/src/fastmcp/server/auth/oidc_proxy.py b/src/fastmcp/server/auth/oidc_proxy.py
index d89ac0756..3a000129f 100644
--- a/src/fastmcp/server/auth/oidc_proxy.py
+++ b/src/fastmcp/server/auth/oidc_proxy.py
@@ -255,8 +255,8 @@ class OIDCProxy(OAuthProxy):
If empty list, no redirect URIs are allowed.
These are for MCP clients performing loopback redirects, NOT for the upstream OAuth app.
client_storage: Storage backend for OAuth state (client registrations, encrypted tokens).
- If None, a DiskStore will be created in the data directory (derived from `platformdirs`). The
- disk store will be encrypted using a key derived from the JWT Signing Key.
+ If None, an encrypted file store will be created in the data directory
+ (derived from `platformdirs`).
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. If not
provided, the upstream client secret will be used to derive a 32-byte key using PBKDF2.
diff --git a/src/fastmcp/server/auth/providers/auth0.py b/src/fastmcp/server/auth/providers/auth0.py
index 42e51ceb9..46c4f70c9 100644
--- a/src/fastmcp/server/auth/providers/auth0.py
+++ b/src/fastmcp/server/auth/providers/auth0.py
@@ -86,8 +86,8 @@ class Auth0Provider(OIDCProxy):
allowed_client_redirect_uris: List of allowed redirect URI patterns for MCP clients.
If None (default), all URIs are allowed. If empty list, no URIs are allowed.
client_storage: Storage backend for OAuth state (client registrations, encrypted tokens).
- If None, a DiskStore will be created in the data directory (derived from `platformdirs`). The
- disk store will be encrypted using a key derived from the JWT Signing Key.
+ If None, an encrypted file store will be created in the data directory
+ (derived from `platformdirs`).
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. If not
provided, the upstream client secret will be used to derive a 32-byte key using PBKDF2.
diff --git a/src/fastmcp/server/auth/providers/aws.py b/src/fastmcp/server/auth/providers/aws.py
index 19f2908aa..d684ee82f 100644
--- a/src/fastmcp/server/auth/providers/aws.py
+++ b/src/fastmcp/server/auth/providers/aws.py
@@ -125,8 +125,8 @@ class AWSCognitoProvider(OIDCProxy):
allowed_client_redirect_uris: List of allowed redirect URI patterns for MCP clients.
If None (default), all URIs are allowed. If empty list, no URIs are allowed.
client_storage: Storage backend for OAuth state (client registrations, encrypted tokens).
- If None, a DiskStore will be created in the data directory (derived from `platformdirs`). The
- disk store will be encrypted using a key derived from the JWT Signing Key.
+ If None, an encrypted file store will be created in the data directory
+ (derived from `platformdirs`).
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. If not
provided, the upstream client secret will be used to derive a 32-byte key using PBKDF2.
diff --git a/src/fastmcp/server/auth/providers/azure.py b/src/fastmcp/server/auth/providers/azure.py
index b5974d887..b651b74ed 100644
--- a/src/fastmcp/server/auth/providers/azure.py
+++ b/src/fastmcp/server/auth/providers/azure.py
@@ -140,8 +140,8 @@ class AzureProvider(OAuthProxy):
allowed_client_redirect_uris: List of allowed redirect URI patterns for MCP clients.
If None (default), all URIs are allowed. If empty list, no URIs are allowed.
client_storage: Storage backend for OAuth state (client registrations, encrypted tokens).
- If None, a DiskStore will be created in the data directory (derived from `platformdirs`). The
- disk store will be encrypted using a key derived from the JWT Signing Key.
+ If None, an encrypted file store will be created in the data directory
+ (derived from `platformdirs`).
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. If not
provided, the upstream client secret will be used to derive a 32-byte key using PBKDF2.
diff --git a/src/fastmcp/server/auth/providers/discord.py b/src/fastmcp/server/auth/providers/discord.py
index a6882be1e..4fb5ebb53 100644
--- a/src/fastmcp/server/auth/providers/discord.py
+++ b/src/fastmcp/server/auth/providers/discord.py
@@ -201,8 +201,8 @@ class DiscordProvider(OAuthProxy):
allowed_client_redirect_uris: List of allowed redirect URI patterns for MCP clients.
If None (default), all URIs are allowed. If empty list, no URIs are allowed.
client_storage: Storage backend for OAuth state (client registrations, encrypted tokens).
- If None, a DiskStore will be created in the data directory (derived from `platformdirs`). The
- disk store will be encrypted using a key derived from the JWT Signing Key.
+ If None, an encrypted file store will be created in the data directory
+ (derived from `platformdirs`).
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. If not
provided, the upstream client secret will be used to derive a 32-byte key using PBKDF2.
diff --git a/src/fastmcp/server/auth/providers/github.py b/src/fastmcp/server/auth/providers/github.py
index ee04a466e..abaaa439a 100644
--- a/src/fastmcp/server/auth/providers/github.py
+++ b/src/fastmcp/server/auth/providers/github.py
@@ -196,8 +196,8 @@ class GitHubProvider(OAuthProxy):
allowed_client_redirect_uris: List of allowed redirect URI patterns for MCP clients.
If None (default), all URIs are allowed. If empty list, no URIs are allowed.
client_storage: Storage backend for OAuth state (client registrations, encrypted tokens).
- If None, a DiskStore will be created in the data directory (derived from `platformdirs`). The
- disk store will be encrypted using a key derived from the JWT Signing Key.
+ If None, an encrypted file store will be created in the data directory
+ (derived from `platformdirs`).
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. If not
provided, the upstream client secret will be used to derive a 32-byte key using PBKDF2.
diff --git a/src/fastmcp/server/auth/providers/google.py b/src/fastmcp/server/auth/providers/google.py
index 3badddc3b..80deac6e3 100644
--- a/src/fastmcp/server/auth/providers/google.py
+++ b/src/fastmcp/server/auth/providers/google.py
@@ -216,8 +216,8 @@ class GoogleProvider(OAuthProxy):
allowed_client_redirect_uris: List of allowed redirect URI patterns for MCP clients.
If None (default), all URIs are allowed. If empty list, no URIs are allowed.
client_storage: Storage backend for OAuth state (client registrations, encrypted tokens).
- If None, a DiskStore will be created in the data directory (derived from `platformdirs`). The
- disk store will be encrypted using a key derived from the JWT Signing Key.
+ If None, an encrypted file store will be created in the data directory
+ (derived from `platformdirs`).
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. If not
provided, the upstream client secret will be used to derive a 32-byte key using PBKDF2.
diff --git a/src/fastmcp/server/auth/providers/workos.py b/src/fastmcp/server/auth/providers/workos.py
index 5c0ba73e1..f34f77029 100644
--- a/src/fastmcp/server/auth/providers/workos.py
+++ b/src/fastmcp/server/auth/providers/workos.py
@@ -162,8 +162,8 @@ class WorkOSProvider(OAuthProxy):
allowed_client_redirect_uris: List of allowed redirect URI patterns for MCP clients.
If None (default), all URIs are allowed. If empty list, no URIs are allowed.
client_storage: Storage backend for OAuth state (client registrations, encrypted tokens).
- If None, a DiskStore will be created in the data directory (derived from `platformdirs`). The
- disk store will be encrypted using a key derived from the JWT Signing Key.
+ If None, an encrypted file store will be created in the data directory
+ (derived from `platformdirs`).
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. If not
provided, the upstream client secret will be used to derive a 32-byte key using PBKDF2.
diff --git a/tests/conftest.py b/tests/conftest.py
index ae3061fe5..1401d4141 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -58,8 +58,8 @@ def enable_fastmcp_logger_propagation(caplog):
def isolate_settings_home(tmp_path: Path):
"""Ensure each test uses an isolated settings.home directory.
- This prevents SQLite database locking issues on Windows when multiple
- tests share the same DiskStore directory in settings.home / "oauth-proxy".
+ This prevents file locking issues when multiple tests share the same
+ storage directory in settings.home / "oauth-proxy".
"""
test_home = tmp_path / "fastmcp-test-home"
test_home.mkdir(exist_ok=True)
diff --git a/tests/server/auth/test_oauth_proxy_storage.py b/tests/server/auth/test_oauth_proxy_storage.py
index 7c898823e..0273a368c 100644
--- a/tests/server/auth/test_oauth_proxy_storage.py
+++ b/tests/server/auth/test_oauth_proxy_storage.py
@@ -1,14 +1,15 @@
"""Tests for OAuth proxy with persistent storage."""
+import tempfile
+import warnings
from collections.abc import AsyncGenerator
from pathlib import Path
from unittest.mock import AsyncMock, Mock
import pytest
-from diskcache.core import tempfile
from inline_snapshot import snapshot
from key_value.aio.protocols import AsyncKeyValue
-from key_value.aio.stores.disk import MultiDiskStore
+from key_value.aio.stores.filetree import FileTreeStore
from key_value.aio.stores.memory import MemoryStore
from mcp.shared.auth import OAuthClientInformationFull
from pydantic import AnyUrl
@@ -29,12 +30,12 @@ class TestOAuthProxyStorage:
return verifier
@pytest.fixture
- async def temp_storage(self) -> AsyncGenerator[MultiDiskStore, None]:
+ async def temp_storage(self) -> AsyncGenerator[FileTreeStore, None]:
"""Create file-based storage for testing."""
with tempfile.TemporaryDirectory() as temp_dir:
- disk_store = MultiDiskStore(base_directory=Path(temp_dir))
- yield disk_store
- await disk_store.close()
+ with warnings.catch_warnings():
+ warnings.simplefilter("ignore", UserWarning)
+ yield FileTreeStore(data_directory=Path(temp_dir))
@pytest.fixture
def memory_storage(self) -> MemoryStore:
diff --git a/tests/server/middleware/test_caching.py b/tests/server/middleware/test_caching.py
index 52e5c90c9..858089251 100644
--- a/tests/server/middleware/test_caching.py
+++ b/tests/server/middleware/test_caching.py
@@ -2,12 +2,18 @@
import sys
import tempfile
+import warnings
+from pathlib import Path
from unittest.mock import AsyncMock, MagicMock
import mcp.types
import pytest
from inline_snapshot import snapshot
-from key_value.aio.stores.disk import DiskStore
+from key_value.aio.stores.filetree import (
+ FileTreeStore,
+ FileTreeV1CollectionSanitizationStrategy,
+ FileTreeV1KeySanitizationStrategy,
+)
from key_value.aio.stores.memory import MemoryStore
from key_value.aio.wrappers.statistics.wrapper import (
GetStatistics,
@@ -281,7 +287,7 @@ class TestResponseCachingMiddleware:
class TestResponseCachingMiddlewareIntegration:
"""Integration tests with real FastMCP server."""
- @pytest.fixture(params=["memory", "disk"])
+ @pytest.fixture(params=["memory", "filetree"])
async def caching_server(
self,
tracking_calculator: TrackingCalculator,
@@ -291,9 +297,21 @@ class TestResponseCachingMiddlewareIntegration:
mcp = FastMCP("CachingTestServer", dereference_schemas=False)
with tempfile.TemporaryDirectory(ignore_cleanup_errors=True) as temp_dir:
- disk_store: DiskStore = DiskStore(directory=temp_dir)
+ with warnings.catch_warnings():
+ warnings.simplefilter("ignore", UserWarning)
+ file_store = FileTreeStore(
+ data_directory=Path(temp_dir),
+ key_sanitization_strategy=FileTreeV1KeySanitizationStrategy(
+ Path(temp_dir)
+ ),
+ collection_sanitization_strategy=FileTreeV1CollectionSanitizationStrategy(
+ Path(temp_dir)
+ ),
+ )
response_caching_middleware = ResponseCachingMiddleware(
- cache_storage=disk_store if request.param == "disk" else MemoryStore(),
+ cache_storage=file_store
+ if request.param == "filetree"
+ else MemoryStore(),
)
mcp.add_middleware(middleware=response_caching_middleware)
@@ -304,8 +322,6 @@ class TestResponseCachingMiddlewareIntegration:
yield mcp
- await disk_store.close()
-
@pytest.fixture
def non_caching_server(self, tracking_calculator: TrackingCalculator):
"""Create a FastMCP server for non-caching tests."""
diff --git a/uv.lock b/uv.lock
index 7fbe94211..7e7976c22 100644
--- a/uv.lock
+++ b/uv.lock
@@ -7,6 +7,18 @@ resolution-markers = [
"python_full_version < '3.11'",
]
+[[package]]
+name = "aiofile"
+version = "3.9.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "caio" },
+]
+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 = "annotated-doc"
version = "0.0.4"
@@ -162,6 +174,25 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/ed/9e/5faefbf9db1db466d633735faceda1f94aa99ce506ac450d232536266b32/cachetools-7.0.1-py3-none-any.whl", hash = "sha256:8f086515c254d5664ae2146d14fc7f65c9a4bce75152eb247e5a9c5e6d7b2ecf", size = 13484, upload-time = "2026-02-10T22:24:03.741Z" },
]
+[[package]]
+name = "caio"
+version = "0.9.25"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/92/88/b8527e1b00c1811db339a1df8bd1ae49d146fcea9d6a5c40e3a80aaeb38d/caio-0.9.25.tar.gz", hash = "sha256:16498e7f81d1d0f5a4c0ad3f2540e65fe25691376e0a5bd367f558067113ed10", size = 26781, upload-time = "2025-12-26T15:21:36.501Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/6a/80/ea4ead0c5d52a9828692e7df20f0eafe8d26e671ce4883a0a146bb91049e/caio-0.9.25-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ca6c8ecda611478b6016cb94d23fd3eb7124852b985bdec7ecaad9f3116b9619", size = 36836, upload-time = "2025-12-26T15:22:04.662Z" },
+ { url = "https://files.pythonhosted.org/packages/17/b9/36715c97c873649d1029001578f901b50250916295e3dddf20c865438865/caio-0.9.25-cp310-cp310-manylinux2010_x86_64.manylinux2014_x86_64.manylinux_2_12_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:db9b5681e4af8176159f0d6598e73b2279bb661e718c7ac23342c550bd78c241", size = 79695, upload-time = "2025-12-26T15:22:18.818Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/90/543f556fcfcfa270713eef906b6352ab048e1e557afec12925c991dc93c2/caio-0.9.25-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d6956d9e4a27021c8bd6c9677f3a59eb1d820cc32d0343cea7961a03b1371965", size = 36839, upload-time = "2025-12-26T15:21:40.267Z" },
+ { url = "https://files.pythonhosted.org/packages/51/3b/36f3e8ec38dafe8de4831decd2e44c69303d2a3892d16ceda42afed44e1b/caio-0.9.25-cp311-cp311-manylinux2010_x86_64.manylinux2014_x86_64.manylinux_2_12_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bf84bfa039f25ad91f4f52944452a5f6f405e8afab4d445450978cd6241d1478", size = 80255, upload-time = "2025-12-26T15:22:20.271Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/25/79c98ebe12df31548ba4eaf44db11b7cad6b3e7b4203718335620939083c/caio-0.9.25-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fb7ff95af4c31ad3f03179149aab61097a71fd85e05f89b4786de0359dffd044", size = 36983, upload-time = "2025-12-26T15:21:36.075Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/2b/21288691f16d479945968a0a4f2856818c1c5be56881d51d4dac9b255d26/caio-0.9.25-cp312-cp312-manylinux2010_x86_64.manylinux2014_x86_64.manylinux_2_12_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:97084e4e30dfa598449d874c4d8e0c8d5ea17d2f752ef5e48e150ff9d240cd64", size = 82012, upload-time = "2025-12-26T15:22:20.983Z" },
+ { url = "https://files.pythonhosted.org/packages/31/57/5e6ff127e6f62c9f15d989560435c642144aa4210882f9494204bc892305/caio-0.9.25-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d6c2a3411af97762a2b03840c3cec2f7f728921ff8adda53d7ea2315a8563451", size = 36979, upload-time = "2025-12-26T15:21:35.484Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/9f/f21af50e72117eb528c422d4276cbac11fb941b1b812b182e0a9c70d19c5/caio-0.9.25-cp313-cp313-manylinux2010_x86_64.manylinux2014_x86_64.manylinux_2_12_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0998210a4d5cd5cb565b32ccfe4e53d67303f868a76f212e002a8554692870e6", size = 81900, upload-time = "2025-12-26T15:22:21.919Z" },
+ { url = "https://files.pythonhosted.org/packages/69/ca/a08fdc7efdcc24e6a6131a93c85be1f204d41c58f474c42b0670af8c016b/caio-0.9.25-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:fab6078b9348e883c80a5e14b382e6ad6aabbc4429ca034e76e730cf464269db", size = 36978, upload-time = "2025-12-26T15:21:41.055Z" },
+ { url = "https://files.pythonhosted.org/packages/5e/6c/d4d24f65e690213c097174d26eda6831f45f4734d9d036d81790a27e7b78/caio-0.9.25-cp314-cp314-manylinux2010_x86_64.manylinux2014_x86_64.manylinux_2_12_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:44a6b58e52d488c75cfaa5ecaa404b2b41cc965e6c417e03251e868ecd5b6d77", size = 81832, upload-time = "2025-12-26T15:22:22.757Z" },
+ { url = "https://files.pythonhosted.org/packages/86/93/1f76c8d1bafe3b0614e06b2195784a3765bbf7b0a067661af9e2dd47fc33/caio-0.9.25-py3-none-any.whl", hash = "sha256:06c0bb02d6b929119b1cfbe1ca403c768b2013a369e2db46bfa2a5761cf82e40", size = 19087, upload-time = "2025-12-26T15:22:00.221Z" },
+]
+
[[package]]
name = "certifi"
version = "2026.1.4"
@@ -598,15 +629,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/bb/8d/dbff05239043271dbeace563a7686212a3dd517864a35623fe4d4a64ca19/dirty_equals-0.11-py3-none-any.whl", hash = "sha256:b1d7093273fc2f9be12f443a8ead954ef6daaf6746fd42ef3a5616433ee85286", size = 28051, upload-time = "2025-11-17T01:51:22.849Z" },
]
-[[package]]
-name = "diskcache"
-version = "5.6.3"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/3f/21/1c1ffc1a039ddcc459db43cc108658f32c57d271d7289a2794e401d0fdb6/diskcache-5.6.3.tar.gz", hash = "sha256:2c3a3fa2743d8535d832ec61c2054a1641f41775aa7c556758a109941e33e4fc", size = 67916, upload-time = "2023-08-31T06:12:00.316Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/3f/27/4570e78fc0bf5ea0ca45eb1de3818a23787af9b390c0b0a0033a1b8236f9/diskcache-5.6.3-py3-none-any.whl", hash = "sha256:5e31b2d5fbad117cc363ebaf6b689474db18a1f6438bc82358b024abd4c2ca19", size = 45550, upload-time = "2023-08-31T06:11:58.822Z" },
-]
-
[[package]]
name = "distro"
version = "1.9.0"
@@ -749,7 +771,7 @@ dependencies = [
{ name = "opentelemetry-api" },
{ name = "packaging" },
{ name = "platformdirs" },
- { name = "py-key-value-aio", extra = ["disk", "keyring", "memory"] },
+ { name = "py-key-value-aio", extra = ["filetree", "keyring", "memory"] },
{ name = "pydantic", extra = ["email"] },
{ name = "pyperclip" },
{ name = "python-dotenv" },
@@ -820,7 +842,7 @@ requires-dist = [
{ name = "opentelemetry-api", specifier = ">=1.20.0" },
{ name = "packaging", specifier = ">=24.0" },
{ name = "platformdirs", specifier = ">=4.0.0" },
- { name = "py-key-value-aio", extras = ["disk", "keyring", "memory"], specifier = ">=0.4.0,<0.5.0" },
+ { name = "py-key-value-aio", extras = ["filetree", "keyring", "memory"], specifier = ">=0.4.4,<0.5.0" },
{ name = "pydantic", extras = ["email"], specifier = ">=2.11.7" },
{ name = "pydocket", marker = "extra == 'tasks'", specifier = ">=0.17.2" },
{ name = "pyperclip", specifier = ">=1.9.0" },
@@ -1641,15 +1663,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/7d/eb/b6260b31b1a96386c0a880edebe26f89669098acea8e0318bff6adb378fd/pathable-0.4.4-py3-none-any.whl", hash = "sha256:5ae9e94793b6ef5a4cbe0a7ce9dbbefc1eec38df253763fd0aeeacf2762dbbc2", size = 9592, upload-time = "2025-01-10T18:43:11.88Z" },
]
-[[package]]
-name = "pathvalidate"
-version = "3.3.1"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/fa/2a/52a8da6fe965dea6192eb716b357558e103aea0a1e9a8352ad575a8406ca/pathvalidate-3.3.1.tar.gz", hash = "sha256:b18c07212bfead624345bb8e1d6141cdcf15a39736994ea0b94035ad2b1ba177", size = 63262, upload-time = "2025-06-15T09:07:20.736Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/9a/70/875f4a23bfc4731703a5835487d0d2fb999031bd415e7d17c0ae615c18b7/pathvalidate-3.3.1-py3-none-any.whl", hash = "sha256:5263baab691f8e1af96092fa5137ee17df5bdfbd6cff1fcac4d6ef4bc2e1735f", size = 24305, upload-time = "2025-06-15T09:07:19.117Z" },
-]
-
[[package]]
name = "pdbpp"
version = "0.12.0.post1"
@@ -1801,21 +1814,21 @@ wheels = [
[[package]]
name = "py-key-value-aio"
-version = "0.4.3"
+version = "0.4.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "beartype" },
{ name = "typing-extensions" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/5b/ee/6a1a37ec67d670e65b349a06089589ddcca93fb0613c316e14b850113074/py_key_value_aio-0.4.3.tar.gz", hash = "sha256:ca1872c19cd84822b45129d66d87f708b116d08cf9e81006196878600eae21a2", size = 92289, upload-time = "2026-02-16T20:55:35.351Z" }
+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" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/44/2c/9e3b718d679e2716ac5dc0ba06cb3dfee42056fbab780b720065bc197b9b/py_key_value_aio-0.4.3-py3-none-any.whl", hash = "sha256:fbf71b193126e136cdfc095fd92ca75e611a1442bad6eb591b8b4fdb36a901c2", size = 152292, upload-time = "2026-02-16T20:55:36.228Z" },
+ { 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" },
]
[package.optional-dependencies]
-disk = [
- { name = "diskcache" },
- { name = "pathvalidate" },
+filetree = [
+ { name = "aiofile" },
+ { name = "anyio" },
]
keyring = [
{ name = "keyring" },
@@ -2700,8 +2713,8 @@ name = "taskgroup"
version = "0.2.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
- { name = "exceptiongroup" },
- { name = "typing-extensions" },
+ { name = "exceptiongroup", marker = "python_full_version < '3.11'" },
+ { name = "typing-extensions", marker = "python_full_version < '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/f0/8d/e218e0160cc1b692e6e0e5ba34e8865dbb171efeb5fc9a704544b3020605/taskgroup-0.2.2.tar.gz", hash = "sha256:078483ac3e78f2e3f973e2edbf6941374fbea81b9c5d0a96f51d297717f4752d", size = 11504, upload-time = "2025-01-03T09:24:13.761Z" }
wheels = [