Skip to content

feat: add LRU credential cache to MCP headers provider with scoped eviction - #5721

Merged
Pratham-Mishra04 merged 1 commit into
devfrom
07-30-feat_cache_per-user_mcp_header_credential_lookups_in_memory
Aug 8, 2026
Merged

Pratham-Mishra04 merged 1 commit into
devfrom
07-30-feat_cache_per-user_mcp_header_credential_lookups_in_memory

Conversation

@Pratham-Mishra04

Copy link
Copy Markdown
Collaborator

Summary

Adds an in-memory LRU cache for per-user MCP header credentials in the mcp_headers provider, eliminating redundant database reads on every tool call for the same (auth mode, identity, MCP client) binding. The cache is bounded at 4096 entries, uses single-flight deduplication to prevent stampedes on concurrent misses, and is invalidated through targeted eviction methods wired into every write path that can change or delete credential rows.

Changes

  • Introduces headerCredentialCache in framework/mcp_headers/credentialcache.go, wrapping the existing lrucache.Cache with a NUL-delimited composite key scheme (mode\x00identity\x00mcpClientID) and a secondary index on credential row ID for targeted eviction without a full scan.
  • GetCredentialByMode now checks the cache before hitting the database. Admin-mode identity is normalized to an empty string before key construction so that stray non-empty identities cannot alias the same row under multiple keys. Both active and needs_update rows are cached with their status preserved; failed lookups (including not-found) are never cached. Every cache hit returns a deep copy of the stored credential so caller mutations cannot corrupt the cached value.
  • UpsertCredential and DeleteCredential evict by credential row ID immediately after their database writes complete.
  • Five scoped eviction methods are exposed on Provider (EvictCredential, EvictCredentialByID, EvictCredentialsByMCPClient, EvictCredentialsByVirtualKey, EvictCredentialsByUser) plus FlushCredentialCache as a coarse fallback.
  • MCPOauthTokenCacheManager is renamed to MCPCredentialCacheManager and extended with EvictMCPHeaderCredentialCacheByID and EvictMCPHeaderCredentialCacheByMCPClient. All handler construction sites and call sites are updated accordingly.
  • BifrostHTTPServer implements the two new MCPCredentialCacheManager methods and calls EvictCredentialsByMCPClient / EvictCredentialsByVirtualKey from RemoveMCPClient, ReloadVirtualKey, and RemoveVirtualKey so cascaded database deletes are reflected in the cache.
  • The updateMCPClient handler evicts by MCP client after a MarkMCPPerUserHeaderCredentialsNeedsUpdate flip and after ReconcileMCPHeadersAfterMCPChange, so cached copies carrying pre-flip status or pre-reconcile rows are not served.
  • The revoke sessions handler evicts by credential ID after deleting a header credential row directly through the configstore.
  • sweepOrphanedCredentials documents why it does not evict: the cache can never hold an orphaned row because the ByMode SQL query filters them out, and the reconcile paths that flip rows to orphaned already evict at flip time.

Type of change

  • Bug fix
  • Feature
  • Refactor
  • Documentation
  • Chore/CI

Affected areas

  • Core (Go)
  • Transports (HTTP)
  • Providers/Integrations
  • Plugins
  • UI (React)
  • Docs

How to test

go test ./framework/mcp_headers/... -v -run TestHeaderCredentialCache
go test ./framework/mcp_headers/... -v -run TestGetCredentialByMode
go test ./...

Key scenarios covered by the new test suite:

  • Cache hit and miss on an empty and populated cache.
  • LRU capacity eviction drops the least-recently-used entry and cleans up its credential-ID index mapping.
  • Exact-key eviction, eviction by credential row ID, by MCP client (including admin-mode bindings), by virtual key (scoped to vk-mode only), and by user (scoped to user-mode only).
  • Single-flight deduplication: a burst of concurrent fills for the same key runs the database handler exactly once and shares the result.
  • Errors are shared with waiters but never cached; a subsequent fill re-runs the handler.
  • A fill that races an eviction does not install its result in the cache (generation guard).
  • Upsert rebinds the credential-ID index to the new row ID so the old ID no longer evicts the rebound entry.
  • Nil-receiver safety on all methods.
  • Integration tests through GetCredentialByMode covering: second call served from cache, isolated deep copy returned to callers, upsert evicts so the next read sees new values, delete evicts so the next read returns not-found, needs_update rows cached with status, negative results not cached, and client-scoped eviction covering admin-mode bindings.

Breaking changes

  • Yes
  • No

MCPOauthTokenCacheManager is renamed to MCPCredentialCacheManager and gains two new methods (EvictMCPHeaderCredentialCacheByID, EvictMCPHeaderCredentialCacheByMCPClient). Any server implementation that embeds or implements MCPOauthTokenCacheManager must be updated to implement the full MCPCredentialCacheManager interface and use the new name.

Related issues

Security considerations

The cache is bounded (default 4096 entries) and uses LRU eviction, so a caller presenting arbitrary session-mode identity strings cannot cause unbounded memory growth. Cached credential values are never shared between callers: every hit returns a deep copy, preventing one caller from reading or mutating another caller's credential headers.

Checklist

  • I read docs/contributing/README.md and followed the guidelines
  • I added/updated tests where appropriate
  • I updated documentation where needed
  • I verified builds succeed (Go and UI)
  • I verified the CI pipeline passes locally if applicable

@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

This was referenced Jul 30, 2026

Pratham-Mishra04 commented Jul 30, 2026

Copy link
Copy Markdown
Collaborator Author

This stack of pull requests is managed by Graphite. Learn more about stacking.

@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Added bounded LRU caching for MCP authentication and header credentials.
    • Added targeted and full cache clearing by credential, client, virtual key, or user.
    • Authentication data now refreshes automatically after credential, client, OAuth, and virtual-key changes.
    • Improved concurrent authentication requests and composite cache-key reliability.
  • Bug Fixes

    • Prevented failed, outdated, or negative lookups from being reused.
    • Improved cache isolation and protection against unintended credential-data changes.
    • Added safer handling for empty or invalid cache configurations.
  • Documentation

    • Clarified credential cache behavior during cleanup and authentication updates.

Walkthrough

Adds an indexed LRU cache for MCP header credentials. Provider lookups use cached copies and invalidate entries after mutations. MCP handlers and server callbacks evict cached credentials during MCP lifecycle operations.

Changes

MCP credential cache

Layer / File(s) Summary
Shared cache-key contract
framework/lrucache/lrucache.go, framework/lrucache/lrucache_test.go, framework/oauth2/usertokencache.go
Adds collision-resistant key encoding and strict decoding. OAuth token keys use the shared helpers.
Credential cache lifecycle
framework/mcp_headers/credentialcache.go, framework/mcp_headers/credentialcache_test.go
Adds bounded LRU storage, single-flight fills, credential indexing, scoped eviction, flushing, nil safety, stale-fill protection, and lifecycle tests.
Provider caching and invalidation
framework/mcp_headers/main.go, framework/mcp_headers/credentialcache_test.go, framework/mcp_headers/sweep.go
Caches normalized lookups, returns deep copies, excludes failed lookups, evicts after mutations, and documents sweep behavior.
Transport-wide cache invalidation
transports/bifrost-http/handlers/*, transports/bifrost-http/server/server.go
Unifies OAuth and header-credential cache management and adds invalidation for MCP, client, virtual-key, rotation, reconciliation, and deletion flows.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant MCPHandler
  participant MCPSessionsHandler
  participant BifrostHTTPServer
  participant Provider
  MCPHandler->>Provider: update or reconcile credentials
  MCPHandler->>BifrostHTTPServer: evict header credentials
  MCPSessionsHandler->>BifrostHTTPServer: evict revoked credential or client
  BifrostHTTPServer->>Provider: invoke scoped cache eviction
  Provider-->>BifrostHTTPServer: invalidate matching cache entries
Loading

Possibly related PRs

Suggested reviewers: akshaydeo, bearts, danpiths

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 29.27% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly summarizes the main change: an LRU credential cache for MCP headers with scoped eviction.
Description check ✅ Passed The description covers the purpose, implementation, testing, breaking changes, security considerations, affected areas, and checklist status.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch 07-30-feat_cache_per-user_mcp_header_credential_lookups_in_memory

Comment @coderabbitai help to get the list of available commands.

@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
framework/mcp_headers/credentialcache_test.go (1)

58-72: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Test access-based LRU eviction.

This test only verifies insertion-order eviction. A FIFO cache also passes it.

Read k1 after filling k1 and k2. Then fill k3 and assert that k2 was evicted.

Proposed test update
-	for i := 1; i <= 3; i++ {
+	for i := 1; i <= 2; i++ {
 		_, err := c.Fill(context.Background(),
 			fmt.Sprintf("k%d", i),
 			fillWithCredential(testCredential(fmt.Sprintf("c%d", i), nil)),
 		)
 		require.NoError(t, err)
 	}
 
+	_, ok := c.Get("k1")
+	require.True(t, ok)
+	_, err := c.Fill(context.Background(), "k3", fillWithCredential(testCredential("c3", nil)))
+	require.NoError(t, err)
+
 	assert.Equal(t, 2, c.Len(), "cache must stay at capacity")
-	_, ok := c.Get("k1")
+	_, ok = c.Get("k2")
 	assert.False(t, ok, "least recently used entry must be evicted")
-	_, ok = c.Get("k2")
+	_, ok = c.Get("k1")
 	assert.True(t, ok)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@framework/mcp_headers/credentialcache_test.go` around lines 58 - 72, Update
the cache test around the Fill loop to access k1 after inserting k1 and k2,
making it the most recently used entry before inserting k3. Keep the capacity
assertion, then assert k2 is absent while k1 and k3 remain present, using the
existing c.Get checks.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@framework/mcp_headers/credentialcache_test.go`:
- Around line 58-72: Update the cache test around the Fill loop to access k1
after inserting k1 and k2, making it the most recently used entry before
inserting k3. Keep the capacity assertion, then assert k2 is absent while k1 and
k3 remain present, using the existing c.Get checks.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 165b8cfb-31a9-4c8b-b473-dae06ed620d7

📥 Commits

Reviewing files that changed from the base of the PR and between 2c5a041 and a5c418f.

📒 Files selected for processing (11)
  • framework/lrucache/lrucache.go
  • framework/lrucache/lrucache_test.go
  • framework/mcp_headers/credentialcache.go
  • framework/mcp_headers/credentialcache_test.go
  • framework/mcp_headers/main.go
  • framework/mcp_headers/sweep.go
  • framework/oauth2/usertokencache.go
  • transports/bifrost-http/handlers/governance.go
  • transports/bifrost-http/handlers/mcp.go
  • transports/bifrost-http/handlers/mcpsessions.go
  • transports/bifrost-http/server/server.go
🚧 Files skipped from review as they are similar to previous changes (9)
  • framework/mcp_headers/sweep.go
  • framework/lrucache/lrucache_test.go
  • framework/oauth2/usertokencache.go
  • transports/bifrost-http/handlers/governance.go
  • transports/bifrost-http/server/server.go
  • transports/bifrost-http/handlers/mcpsessions.go
  • framework/lrucache/lrucache.go
  • framework/mcp_headers/main.go
  • transports/bifrost-http/handlers/mcp.go

@Pratham-Mishra04
Pratham-Mishra04 force-pushed the 07-30-feat_cache_per-user_mcp_oauth_token_lookups_in_memory branch from 2c5a041 to c50dff4 Compare August 8, 2026 08:43
@Pratham-Mishra04
Pratham-Mishra04 force-pushed the 07-30-feat_cache_per-user_mcp_header_credential_lookups_in_memory branch from a5c418f to 8157cc2 Compare August 8, 2026 08:43

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@framework/lrucache/lrucache.go`:
- Around line 427-432: Update DecodeKey to reject any n greater than len(key)/2
before allocating the parts slice, while preserving the existing negative-count
rejection and valid decoding behavior. Add a regression test covering an
oversized count, such as DecodeKey("", 1<<30), and verify it returns false
without attempting a large allocation.
🪄 Autofix

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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: f11bf2e1-149a-41b9-85cf-e7ca80063d8a

📥 Commits

Reviewing files that changed from the base of the PR and between a5c418f and 8157cc2.

📒 Files selected for processing (2)
  • framework/lrucache/lrucache.go
  • framework/lrucache/lrucache_test.go

Comment on lines +427 to +432
func DecodeKey(key string, n int) (parts []string, ok bool) {
if n < 0 {
return nil, false
}
rest := key
parts = make([]string, 0, n)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Reject infeasible part counts before allocation.

Every encoded part consumes at least two bytes. A valid key requires n <= len(key)/2.

DecodeKey("", 1<<30) currently attempts a large allocation before it returns false. This can panic or exhaust memory if a caller passes an untrusted count. Reject the count before make, and add a regression test.

Proposed fix
 func DecodeKey(key string, n int) (parts []string, ok bool) {
-	if n < 0 {
+	if n < 0 || n > len(key)/2 {
 		return nil, false
 	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func DecodeKey(key string, n int) (parts []string, ok bool) {
if n < 0 {
return nil, false
}
rest := key
parts = make([]string, 0, n)
func DecodeKey(key string, n int) (parts []string, ok bool) {
if n < 0 || n > len(key)/2 {
return nil, false
}
rest := key
parts = make([]string, 0, n)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@framework/lrucache/lrucache.go` around lines 427 - 432, Update DecodeKey to
reject any n greater than len(key)/2 before allocating the parts slice, while
preserving the existing negative-count rejection and valid decoding behavior.
Add a regression test covering an oversized count, such as DecodeKey("", 1<<30),
and verify it returns false without attempting a large allocation.

Pratham-Mishra04 commented Aug 8, 2026

Copy link
Copy Markdown
Collaborator Author

Merge activity

  • Aug 8, 8:47 AM UTC: A user started a stack merge that includes this pull request via Graphite.
  • Aug 8, 9:39 AM UTC: Graphite rebased this pull request as part of a merge.
  • Aug 8, 9:40 AM UTC: @Pratham-Mishra04 merged this pull request with Graphite.

@Pratham-Mishra04
Pratham-Mishra04 changed the base branch from 07-30-feat_cache_per-user_mcp_oauth_token_lookups_in_memory to graphite-base/5721 August 8, 2026 09:35
@Pratham-Mishra04
Pratham-Mishra04 changed the base branch from graphite-base/5721 to dev August 8, 2026 09:37
@Pratham-Mishra04
Pratham-Mishra04 requested a review from a team as a code owner August 8, 2026 09:37
…equest unblocks instead of waiting on an unrelated leader
@Pratham-Mishra04
Pratham-Mishra04 force-pushed the 07-30-feat_cache_per-user_mcp_header_credential_lookups_in_memory branch from 8157cc2 to 91ea2dd Compare August 8, 2026 09:38
@Pratham-Mishra04
Pratham-Mishra04 merged commit 9e6703d into dev Aug 8, 2026
14 of 15 checks passed
@Pratham-Mishra04
Pratham-Mishra04 deleted the 07-30-feat_cache_per-user_mcp_header_credential_lookups_in_memory branch August 8, 2026 09:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants