Add Firestore backend for FastMCP OAuth proxy client storage#745
Add Firestore backend for FastMCP OAuth proxy client storage#745anton-jackson wants to merge 4 commits into
Conversation
Persists JTI mappings and upstream token sets in Cloud Firestore so OAuthProxy state survives Cloud Run instance restarts and scale-to-zero, eliminating the silent 401 invalid_token failures that surface as opaque tool errors when the in-memory store is wiped. - New FirestoreStore (auth/firestore_kv_store.py) implementing the py-key-value-aio BaseStore interface against google-cloud-firestore; one Firestore collection holds compound-keyed docs with an expires_at Timestamp for TTL-policy-driven auto-cleanup. - Wired as a "firestore" client_storage backend in core/server.py alongside valkey/disk/memory, with the same FernetEncryptionWrapper for at-rest encryption. - Added google-cloud-firestore as an optional [firestore] extra and enabled it in the Dockerfile alongside the existing disk extra. Note: requires regenerating uv.lock with the firestore extra (uv lock --extra firestore) and granting the Cloud Run service account roles/datastore.user on a project with Firestore Native enabled. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
uv.lock does not yet include google-cloud-firestore, so 'uv sync --frozen --extra firestore' fails in CI. Install it via 'uv pip install' as a temporary unblock; revert once uv.lock is regenerated.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (2)
🚧 Files skipped from review as they are similar to previous changes (1)
📝 WalkthroughWalkthroughAdds a Firestore-backed key/value store and integrates it as an optional OAuth client storage backend (with Fernet envelope encryption), declares the Firestore optional dependency and installs it in the Docker build, and includes a small unrelated refactor in Gmail helpers. ChangesFirestore Storage + Integration
Minor Gmail Helper Refactor
Sequence DiagramsequenceDiagram
participant Server as Server (core/server.py)
participant Env as Env (WORKSPACE_MCP_OAUTH_PROXY_FIRESTORE_*)
participant Store as FirestoreStore (auth/firestore_kv_store.py)
participant GCF as GoogleCloudFirestore
participant Encrypt as FernetEncryptionWrapper
Server->>Env: Read Firestore project/database/collection
Env-->>Server: Firestore params
Server->>Store: Initialize FirestoreStore(project, database, collection)
Store->>GCF: Connect / read/write documents
Server->>Server: Derive storage_encryption_key from JWT signing key
Server->>Encrypt: Wrap FirestoreStore with Fernet key
Encrypt-->>Server: Encrypted client_storage ready
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested labels
Poem
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
⚔️ Resolve merge conflicts
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Review rate limit: 7/8 reviews remaining, refill in 7 minutes and 30 seconds.Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
auth/firestore_kv_store.py (1)
67-120: ⚡ Quick winPlease add async coverage for the expiry and CRUD semantics here.
This class owns custom logic for expired reads and the
collection:keydocument mapping, but there’s no regression net for it in the PR. A small async test aroundput/get/deleteplus an expired-document read would catch the main failure modes before they surface in OAuth DCR flows.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@auth/firestore_kv_store.py` around lines 67 - 120, Add an async test module that exercises the FirestoreKVStore CRUD and expiry behavior: call _put_managed_entry to write a ManagedEntry (use the same serialization adapter to produce JSON), then verify _get_managed_entry returns the entry; delete it and assert _delete_managed_entry returns True and subsequent _get_managed_entry returns None; also write a ManagedEntry with expires_at set in the past and assert _get_managed_entry returns None (and that a future expires_at is preserved and returned). Use the class's helper methods (_put_managed_entry, _get_managed_entry, _delete_managed_entry) and fields (_EXPIRES_AT_FIELD, _JSON_FIELD) to construct/inspect payloads and run the tests as async unit tests to cover TTL/eventual deletion semantics and collection:key mapping.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@core/server.py`:
- Around line 513-518: The current except ImportError block in core/server.py
just logs a warning (logger.warning) when google-cloud-firestore import fails;
instead, if the configuration explicitly requests firestore
(WORKSPACE_MCP_OAUTH_PROXY_STORAGE_BACKEND == "firestore"), fail startup
immediately rather than falling back: replace the warning-only behavior with
logic that checks the WORKSPACE_MCP_OAUTH_PROXY_STORAGE_BACKEND env/config value
and, when it equals "firestore", log an error and re-raise the ImportError or
call sys.exit(1)/raise RuntimeError with a clear message including exc;
otherwise (when not explicitly requested) keep the existing fallback behavior or
warning.
In `@Dockerfile`:
- Around line 16-20: The Dockerfile currently bypasses the lock by running `uv
pip install "google-cloud-firestore..."`; instead update the `uv sync`
invocation to use the declared extra and remove the separate `uv pip install`
line: replace the `RUN uv sync --frozen --no-dev --extra disk`/`RUN uv pip
install ...` pattern with a single `uv sync --frozen --no-dev --extra firestore`
(or include both extras if `disk` is still needed) and delete the `uv pip
install` line; also update the comment to reflect that `uv.lock` includes the
`firestore` extra so the lockfile is honored.
---
Nitpick comments:
In `@auth/firestore_kv_store.py`:
- Around line 67-120: Add an async test module that exercises the
FirestoreKVStore CRUD and expiry behavior: call _put_managed_entry to write a
ManagedEntry (use the same serialization adapter to produce JSON), then verify
_get_managed_entry returns the entry; delete it and assert _delete_managed_entry
returns True and subsequent _get_managed_entry returns None; also write a
ManagedEntry with expires_at set in the past and assert _get_managed_entry
returns None (and that a future expires_at is preserved and returned). Use the
class's helper methods (_put_managed_entry, _get_managed_entry,
_delete_managed_entry) and fields (_EXPIRES_AT_FIELD, _JSON_FIELD) to
construct/inspect payloads and run the tests as async unit tests to cover
TTL/eventual deletion semantics and collection:key mapping.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: e95a47b8-8161-422b-b69d-6ab339179999
⛔ Files ignored due to path filters (1)
uv.lockis excluded by!**/*.lock
📒 Files selected for processing (5)
Dockerfileauth/firestore_kv_store.pycore/server.pygmail/gmail_helpers.pypyproject.toml
- Dockerfile: restore single 'uv sync --frozen --extra firestore' now that uv.lock includes google-cloud-firestore. - core/server.py: fail fast when firestore backend is requested but the package is missing, instead of silently falling back to the ephemeral default (which would cause client registrations to vanish on restart in multi-instance deployments).
Summary
Adds an optional Firestore-backed key/value store for the FastMCP OAuth proxy's dynamic client registration data, so deployments running on serverless / multi-instance infrastructure (e.g. Cloud Run) can persist registered clients across instances and restarts instead of relying on local disk.
What changed
auth/firestore_kv_store.py— new async KV store implementing the FastMCP storage interface, backed bygoogle-cloud-firestore.core/server.py— wires the Firestore store in when configured; falls back to the existing disk store otherwise.pyproject.toml— adds afirestoreextra (google-cloud-firestore>=2.16.0).Dockerfile— installs the new extra. Note:google-cloud-firestoreis installed viauv pip installafteruv sync --frozenbecauseuv.lockhas not yet been regenerated to include it. This is a temporary workaround — onceuv lockis run with PyPI access, theRUN uv pip installline should be removed and the extra folded back intouv sync --frozen --extra firestore.gmail/gmail_helpers.py— minor lint/format fixes from ruff.Test plan
uv sync+ firestore install both succeedSummary by CodeRabbit
New Features
Chores
Code Quality