Skip to content

Conversation

strawgate
Copy link
Collaborator

@strawgate strawgate commented Sep 24, 2025

Description

Adds KV data storage using https://github.com/strawgate/py-key-value. py-key-value provides disk, memory, Elasticsearch, Memcached, MongoDB, Redis, and Valkey support.

Switches OAuth token and client token storage to using the py-key-value library. Defaults to memory storage unless a user provides a key_value store to the OAuth components.

Breaking changes should just be the loss of json-on-disk tokens and the arg change from token_storage_cache_dir to token_storage

Contributors Checklist

  • I have followed the repository's development workflow
  • I have tested my changes manually and by adding relevant tests
  • I have performed all required documentation updates

Review Checklist

  • I have self-reviewed my changes
  • My Pull Request is ready for review

@strawgate strawgate requested review from Copilot and jlowin September 24, 2025 23:15
Copy link
Contributor

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR migrates OAuth storage from custom file-based storage implementations to py-kv-store-adapter, standardizing data persistence across the codebase. The change introduces a default disk storage location at ~/.fastmcp/data.db and switches the OAuth implementation to use the new KV store adapters.

  • Replace custom JSONFileStorage and InMemoryStorage with py-kv-store-adapter implementations
  • Add data_store property to settings that provides a DiskStore instance with 10MB size limit
  • Update OAuth proxy and client authentication to use the new storage adapters with proper collections

Reviewed Changes

Copilot reviewed 15 out of 16 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
pyproject.toml Add kv-store-adapter dependency
src/fastmcp/settings.py Add data_path and data_store property for centralized storage
src/fastmcp/utilities/storage.py Remove custom storage implementations
src/fastmcp/server/auth/oauth_proxy.py Migrate to PydanticAdapter with DiskStore for client storage
src/fastmcp/client/auth/oauth.py Replace FileTokenStorage with TokenStorageAdapter using KV store
tests/utilities/test_storage.py Remove tests for deleted storage classes
tests/server/auth/test_oauth_proxy_storage.py Update tests to use DiskStore and MemoryStore
tests/server/auth/test_oauth_proxy_redirect_validation.py Fix attribute access for allowed_redirect_uri_patterns
tests/client/auth/test_oauth_token_expiry.py Comment out tests that depend on removed FileTokenStorage

@marvin-context-protocol marvin-context-protocol bot added enhancement Improvement to existing functionality. For issues and smaller PR improvements. auth Related to authentication (Bearer, JWT, OAuth, WorkOS) for client or server. labels Sep 24, 2025
@strawgate strawgate changed the title Switch OAuth storage to py-kv-store-adapter implementation Add data_store to FastMCP and switch OAuth Storage to use it Sep 24, 2025
@ruhulio
Copy link
Contributor

ruhulio commented Sep 25, 2025

@strawgate I tested this out with the 0.1.2 changes and the DiskStore, MemoryStore, RedisStore, and Simple*Store. They all worked as expected.

I currently don't have quick access to an ElasticSearch instance so was unable to test against that.

@strawgate
Copy link
Collaborator Author

Thank you! I can test with Elasticsearch for sure -- but if for whatever reason you'd like free access to an Elasticsearch project in Elastic Cloud for a bit to play around you just let me know :)

@jlowin
Copy link
Owner

jlowin commented Sep 25, 2025

This looks great and is well thought out. My only comment is that "data_path" is not a particularly descriptive setting name (nor the data_store calculated property) -- on the one hand, it is literally true that it's the canonical place where FastMCP will write data, but I think for users it could easily be confused with whatever data / resources they are exposing through MCP. Perhaps storage_path? I know I'm splitting hairs here. Also I think documenting this field (and the None behavior) with a Pydantic description would be excellent

@strawgate
Copy link
Collaborator Author

I'm going to do a little refactor of the adapter library today and then will update the pr

@strawgate strawgate changed the title Add data_store to FastMCP and switch OAuth Storage to use it Add Storage to FastMCP and switch OAuth to use it Sep 29, 2025
Copy link
Contributor

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

Copilot reviewed 15 out of 16 changed files in this pull request and generated 3 comments.

@dexalex84
Copy link

Hi

Tried your version with storage. I need this because our version in prod required multiple running nodes in K8S.
Without is I'm getting client_id not found during auth phase very often.

So this improvement expected to help - and file storage is not available for us.

I have tried with Redis and DragonflyDB - initially it works locally (1 node) and K8S right after deployment - when cache is empty.

But after token timeout ( I use OKTA with TTL 1 hour + Cursor AI as client )

It's showed that I'm logged off. Usually on memory based storage ( default OAuth Proxy) it was enough to reset client token in Cursor AI - and it forces to log off and reauth works fine.

But in this version it showed in Cursor AI output MCP:
[error] Failed to complete OAuth exchange authorization code does not exist

And can not fix this without redeploying again.

Different behavior, I'm trying figure out it.

BTW:
Is this correct cache in Redis data:

{
    "created_at": "2025-09-28T15:48:44.166440+00:00",
    "ttl": null,
    "expires_at": null,
    "collection":
    "oauth-proxy-clients",
    "key": "b59f86b2-aa94-4115-9ef8-XXXX",
    "value": {"redirect_uris": ["cursor://anysphere.cursor-retrieval/oauth/user-XXXX/callback"],
        "token_endpoint_auth_method": "none",
        "grant_types": ["authorization_code", "refresh_token"],
        "response_types": ["code"],
         "scope": "openid profile email",
         "client_name": null,
         "client_uri": null, "logo_uri": null, "contacts": null, "tos_uri": null,
         "policy_uri": null, "jwks_uri": null, "jwks": null, "software_id": null,
         "software_version": null,
         "client_id": "b59f86b2-aa94-4115-9ef8-XXXX",
         "client_secret": null,
         "client_id_issued_at": null,
         "client_secret_expires_at": null,
         "allowed_redirect_uri_patterns": ["http://localhost:8000", "cursor://anysphere.cursor-retrieval*"]
        }
 }

I wonder is this expected to have some many NULLs

Context:

using_redis_url = f"redis://{settings.redis_host}:6379"
redis_store = RedisStore(url=using_redis_url)


 token_verifier = JWTVerifier(
    jwks_uri=settings.jwks_uri,  
    issuer=settings.issuer,     
    audience=settings.audience,  
    base_url=settings.base_url, 
    required_scopes=['openid', 'profile', 'email'] 
)

auth = OAuthProxy(
    # Provider's OAuth endpoints (from their documentation)
    upstream_authorization_endpoint=settings.upstream_authorization_endpoint, # pragma: allowlist secret
    upstream_token_endpoint=settings.upstream_token_endpoint, # pragma: allowlist secret
    upstream_revocation_endpoint = settings.upstream_revocation_endpoint, # pragma: allowlist secret
    allowed_client_redirect_uris=allowed_redirect_uri,

    # Your registered app credentials
    upstream_client_id=settings.client_id,      # pragma: allowlist secret
    upstream_client_secret=settings.secret_id,  # pragma: allowlist secret

    # Token validation (see Token Verification guide)
    token_verifier=token_verifier,

    # Your FastMCP server's public URL
    base_url=settings.base_url,
    
    # Optional: customize the callback path (default is "/auth/callback")
    redirect_path="/auth/callback",
    client_storage = redis_store
)

@strawgate
Copy link
Collaborator Author

strawgate commented Sep 29, 2025

I'll take a look at this but I did push an update at around the time you may have been testing

Can you:

  1. confirm this doesn't happen when run main with only one pod and
  2. confirm that you have grabbed the latest changes?

@strawgate
Copy link
Collaborator Author

strawgate commented Sep 29, 2025

Also do you have a client other than cursor you can try with? https://forum.cursor.com/t/ver-1-6-42-mcp-oauth-fails-on-error-failed-to-complete-oauth-exchange-authorization-code-does-not-exist/134328

Or can you rollback to a previous version as suggested in that post and see if you get the same behavior

@strawgate
Copy link
Collaborator Author

@ruhulio did you test through a token refresh?

@ruhulio
Copy link
Contributor

ruhulio commented Sep 29, 2025

@ruhulio did you test through a token refresh?

@strawgate I had in my previous testing. I can test things out again later today with the latest updates to the KV package.

@dexalex84
Copy link

I'll take a look at this but I did push an update at around the time you may have been testing

Can you:

  1. confirm this doesn't happen when run main with only one pod and
  2. confirm that you have grabbed the latest changes?

yep

Planing to try it on the latest version today-tomorrow.

  • I will try to test on Cloud Code as alternative

For 1 node it worked fine locally, but I have not tested in pod environment for 1 node yet (will test as well)

@strawgate
Copy link
Collaborator Author

Removing central configuration of storage via settings for now and will follow-up with another PR for configuring storage via the FastMCP class

@strawgate strawgate requested a review from Copilot October 6, 2025 16:52
Copy link
Contributor

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

Copilot reviewed 15 out of 16 changed files in this pull request and generated 2 comments.

@dexalex84
Copy link

Removing central configuration of storage via settings for now and will follow-up with another PR for configuring storage via the FastMCP class

Hi @strawgate, thanks for the update

I see "kv_store_adapter" has been changed to "py-key-value-aio"
Will it have an option to use Redis or Redis like storage?

Just for context I've been using:

from kv_store_adapter.stores.redis import RedisStore
using_redis_url = f"redis://{settings.redis_host}:6379"
redis_store = RedisStore(url=using_redis_url)
....

auth_oidc = OIDCProxy(
     ......
    client_storage = redis_store
)

Because in my installation I can not use persistent disk storage, so using Redis is preferable to omit "client_id" not found error

@strawgate
Copy link
Collaborator Author

strawgate commented Oct 7, 2025

Removing central configuration of storage via settings for now and will follow-up with another PR for configuring storage via the FastMCP class

Hi @strawgate, thanks for the update

I see "kv_store_adapter" has been changed to "py-key-value-aio" Will it have an option to use Redis or Redis like storage?

Just for context I've been using:

from kv_store_adapter.stores.redis import RedisStore
using_redis_url = f"redis://{settings.redis_host}:6379"
redis_store = RedisStore(url=using_redis_url)
....

auth_oidc = OIDCProxy(
     ......
    client_storage = redis_store
)

Because in my installation I can not use persistent disk storage, so using Redis is preferable to omit "client_id" not found error

Yes, I just renamed the library that's all, there's still a redis store

The comment above was regarding the ability to configure some storage settings on the FastMCP.settings object and have that propagate through the various components as a fallback if nothing was passed for client_storage

@strawgate
Copy link
Collaborator Author

@jlowin this is ready for your review

Copy link
Owner

@jlowin jlowin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this looks great! Minor question on docstring and I recommend updating the PR body to detail:

  1. any breaking changes for users upgrading to 2.13 (I think mainly the token storage kwarg? or maybe nobody actually sets that in practice) that we'll copy into the release notes
  2. the PR body still says the default is disk based but now it is memory based so just want to keep that aligned

Lastly one question I wanted to check - does the pydantic adapter fail gracefully e.g. if in 2.13.1 we add a required field to the stored DCR Client details and someone has cached clients from 2.13.0, will the storage raise an error because a field is missing (which would interrupt the auth flow permanently), or will it silently fail and act as if no cached client was found (which would permit the re-auth flow and presumably overwrite the corrupt/old client on the next pass)

"""
super().__init__(*args, **kwargs)
self._allowed_redirect_uri_patterns = allowed_redirect_uri_patterns
allowed_redirect_uri_patterns: list[str] | None = Field(default=None)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor request to copy the deleted argument docstring into the field description

@nkartashov
Copy link

nkartashov commented Oct 8, 2025

@strawgate
Hey, I think there is a related issue in OAuthProxy which can be resolved by moving to using a shared storage: #2010

In an OAuth proxy setup a client makes /authorize call but then the IdP will be making the /callback call to the server. Both these calls touch _oauth_transactions to set and fetch transaction data. Practically it means that both these calls might not land into the same node if one's running a multinode service which results in the auth flow breaking down and it seems that the only way to fix this is to save this data in shared storage as well.
I think the same logic applies to _client_codes and, to a smaller extent, to _access_tokens/_refresh_tokens.
Would you consider improving this either in this PR or a quick followup?

Ideally in the process of this I'd separate the handling of per-user data (client codes, access/refresh tokens) and everything else (oauth_transactions, clients).

@whitehat101
Copy link

Since we can configure auth entirely through environment variables, can we also configure the storage backend entirely through environment variables?

Something like:

FASTMCP_SERVER_STORAGE=redis
FASTMCP_SERVER_STORAGE_REDIS_URI=redis://foo:6379/25

@strawgate
Copy link
Collaborator Author

I think this looks great! Minor question on docstring and I recommend updating the PR body to detail:

  1. any breaking changes for users upgrading to 2.13 (I think mainly the token storage kwarg? or maybe nobody actually sets that in practice) that we'll copy into the release notes
  2. the PR body still says the default is disk based but now it is memory based so just want to keep that aligned

Lastly one question I wanted to check - does the pydantic adapter fail gracefully e.g. if in 2.13.1 we add a required field to the stored DCR Client details and someone has cached clients from 2.13.0, will the storage raise an error because a field is missing (which would interrupt the auth flow permanently), or will it silently fail and act as if no cached client was found (which would permit the re-auth flow and presumably overwrite the corrupt/old client on the next pass)

Will fix, breaking changes should just be the loss of json-based tokens and the arg name change.

Right now it will raise an error on validation failure but I'll add an argument to the Pydantic adapter which you can use to bypass validation errors

@strawgate
Copy link
Collaborator Author

Since we can configure auth entirely through environment variables, can we also configure the storage backend entirely through environment variables?

Something like:

FASTMCP_SERVER_STORAGE=redis
FASTMCP_SERVER_STORAGE_REDIS_URI=redis://foo:6379/25

I'll be adding "central storage configuration" in a followup PR and we can consider something like this

@jlowin
Copy link
Owner

jlowin commented Oct 8, 2025

Awesome! This is such a major feature for 2.13. Approved, merge whenever you feel ready!

@strawgate strawgate requested a review from Copilot October 9, 2025 13:20
Copy link
Contributor

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

Copilot reviewed 15 out of 16 changed files in this pull request and generated 3 comments.

"""
super().__init__(*args, **kwargs)
self._allowed_redirect_uri_patterns = allowed_redirect_uri_patterns
allowed_redirect_uri_patterns: list[str] | None = Field(default=None)
Copy link

Copilot AI Oct 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ProxyDCRClient class has been refactored to use a Field attribute instead of a constructor parameter. This is a breaking change that removes the ability to pass allowed_redirect_uri_patterns during initialization, which could affect code that creates ProxyDCRClient instances directly.

Suggested change
allowed_redirect_uri_patterns: list[str] | None = Field(default=None)
allowed_redirect_uri_patterns: list[str] | None = None

Copilot uses AI. Check for mistakes.

scopes: str | list[str] | None = None,
client_name: str = "FastMCP Client",
token_storage_cache_dir: Path | None = None,
token_storage: AsyncKeyValue | None = None,
Copy link

Copilot AI Oct 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Breaking change: The parameter name changed from token_storage_cache_dir to token_storage and the type changed from Path | None to AsyncKeyValue | None. This will break existing code that passes the old parameter name or a Path object.

Copilot uses AI. Check for mistakes.


DuplicateBehavior = Literal["warn", "error", "replace", "ignore"]

TEN_MB_IN_BYTES = 1024 * 1024 * 10
Copy link

Copilot AI Oct 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The constant TEN_MB_IN_BYTES is defined but never used in any of the changed files. Consider removing it if it's not needed, or add a comment explaining its intended purpose.

Suggested change
TEN_MB_IN_BYTES = 1024 * 1024 * 10

Copilot uses AI. Check for mistakes.

@strawgate strawgate merged commit d46affb into main Oct 9, 2025
9 checks passed
@strawgate strawgate deleted the switch-kvstore branch October 9, 2025 13:24
@strawgate
Copy link
Collaborator Author

🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
auth Related to authentication (Bearer, JWT, OAuth, WorkOS) for client or server. feature Major new functionality. Reserved for 2-4 significant PRs per release. Not for issues.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants