Skip to content

feat: retry shared-connection MCP auth failures after bounded reconnect wait with concurrent dedup - #5726

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

feat: retry shared-connection MCP auth failures after bounded reconnect wait with concurrent dedup#5726
Pratham-Mishra04 merged 1 commit into
devfrom
07-30-feat_heal_shared_mcp_connections_through_token_expiry_without_failed_calls

Conversation

@Pratham-Mishra04

Copy link
Copy Markdown
Collaborator

Summary

On shared MCP connections, a 401 auth failure previously triggered a background reconnect but returned the original error immediately with no retry. This PR upgrades that path to a bounded-wait retry: the caller waits up to MCPSharedAuthRetryReconnectBudget (10 s, further capped by the request's own remaining deadline) for the reconnect to finish, then retries the same call once on the healed connection. If the reconnect exceeds the budget it keeps running in the background and the original error surfaces as before. Concurrent 401s on the same client now deduplicate: the loser of the reconnect race joins the winner's in-flight attempt via a new AwaitReconnect interface method rather than failing outright.

Additionally, OAuthTokenRefreshWorker (renamed from TokenRefreshWorker) gains a SetOnTokenRefreshed callback. The HTTP server installs this callback after the boot dial to proactively recycle shared MCP connections whenever the background token refresh worker lands a fresh credential, instead of waiting for the next call to fail with a 401.

Changes

  • inflightClientOp struct and beginExclusiveClientOp: Replaced the sync.Map[string]bool sentinel in MCPManager with a sync.Map[string]*inflightClientOp that carries a done channel and final error. All exclusive-operation entry points (ReconnectClient, DisableClient, EnableClient, UpdateClient, UpdateClientConnection) now use beginExclusiveClientOp and propagate their return error to waiters via a deferred finish(retErr).

  • AwaitReconnect: New method on MCPManager (and ClientManager interface) that lets a caller block up to a given budget for an in-flight exclusive operation to complete, returning the operation's final error. A timed-out wait never cancels the underlying operation.

  • recoverSharedConnection: New method extracted from attemptAuthFailureRecovery that implements the bounded-wait retry for shared connections. It triggers the background reconnect, waits for the result channel, falls back to AwaitReconnect if the trigger lost the race to a concurrent reconnect, then re-acquires the healed connection and retries the call once.

  • triggerBackgroundReconnect: Now returns a <-chan error (buffered, capacity 1) so the caller can observe the reconnect outcome without blocking the goroutine.

  • Retry opt-out ordering: The destructive/non-idempotent tool gate now sets a retryOptedOut flag rather than returning early, so connection healing always runs on the shared path even when the retry itself is suppressed.

  • OAuthTokenRefreshWorker rename and SetOnTokenRefreshed callback: TokenRefreshWorker is renamed to OAuthTokenRefreshWorker throughout. A new SetOnTokenRefreshed(func(mcpClientID, authMode string)) method (backed by an atomic.Pointer) lets callers register a hook invoked after each successful proactive refresh. The HTTP server uses this to trigger a background ReconnectMCPClient for shared-auth clients immediately after a token is refreshed.

  • Test coverage: Replaced the single TestExecuteTool_AuthFailureRetry_Shared_FailsFastAndTriggersBackgroundReconnect test with four focused tests covering: successful bounded-wait retry, budget-exceeded fallback, reconnect-failed fallback, destructive-tool reconnect-without-retry, and concurrent 401 deduplication. The authRetryClientManager mock now implements the full inflightClientOp dedup logic to faithfully simulate the real manager. New OAuthTokenRefreshWorker tests cover the callback firing, non-firing on failure, non-firing for tokens with no MCP client ID, and nil-safety.

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 ./core/mcp/... ./framework/oauth2/...

Key scenarios exercised by the new tests:

  • A shared-connection 401 where the reconnect completes within budget: expect 2 CallTool invocations and a successful response.
  • A shared-connection 401 where the reconnect is held open past the caller deadline: expect 1 CallTool invocation, the original error, and the reconnect still running.
  • Two concurrent 401s on the same client: expect exactly 1 actual ReconnectClient call, 1 rejected duplicate, 4 total CallTool invocations, and both callers succeeding.
  • A destructive non-idempotent tool: expect no retry but the background reconnect still fires.

Breaking changes

  • Yes
  • No

TokenRefreshWorker is renamed to OAuthTokenRefreshWorker and the Config field TokenRefreshWorker is renamed to OAuthTokenRefreshWorker. Any code referencing these names directly must be updated. The ClientManager interface gains a new required method AwaitReconnect(clientID string, budget time.Duration) (bool, error); all implementations (including mocks) must add this method.

Security considerations

The bounded-wait retry re-uses the caller's existing request context deadline to cap the reconnect wait, preventing a malicious or slow upstream from holding a request open indefinitely beyond its own timeout. The SetOnTokenRefreshed callback is stored as an atomic.Pointer to avoid data races between the worker goroutine and the serving layer installing the callback after startup.

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.

@Pratham-Mishra04
Pratham-Mishra04 force-pushed the 07-30-feat_heal_shared_mcp_connections_through_token_expiry_without_failed_calls branch from 4cfec68 to e340bd6 Compare August 6, 2026 21:53
@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.

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 `@core/mcp/toolmanager.go`:
- Around line 934-940: Update the retry logic around GetClientForTool to
reacquire the original MCP client via GetClientByName(executionConfig.Name)
after ReconnectClient. Reject the retry when the client is missing,
executionConfig.ID differs from the original, or the tool is no longer
available; preserve provider isolation. Add coverage for routing changes during
reconnect and verify the second client receives no retry.
🪄 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: 49db4186-949b-48e5-801e-958146a91c9b

📥 Commits

Reviewing files that changed from the base of the PR and between 01d9624 and e340bd6.

📒 Files selected for processing (14)
  • core/mcp/agent_test.go
  • core/mcp/auth_retry_test.go
  • core/mcp/clientmanager.go
  • core/mcp/codemode/starlark/starlark_test.go
  • core/mcp/mcp.go
  • core/mcp/toolmanager.go
  • core/mcp/toolmanager_test.go
  • framework/configstore/rdb.go
  • framework/configstore/store.go
  • framework/oauth2/sync.go
  • framework/oauth2/sync_test.go
  • framework/oauth2/tokenexchange.go
  • transports/bifrost-http/lib/config.go
  • transports/bifrost-http/server/server.go
🚧 Files skipped from review as they are similar to previous changes (13)
  • framework/configstore/store.go
  • framework/oauth2/tokenexchange.go
  • framework/configstore/rdb.go
  • core/mcp/codemode/starlark/starlark_test.go
  • core/mcp/toolmanager_test.go
  • transports/bifrost-http/server/server.go
  • core/mcp/mcp.go
  • framework/oauth2/sync.go
  • core/mcp/agent_test.go
  • framework/oauth2/sync_test.go
  • core/mcp/clientmanager.go
  • transports/bifrost-http/lib/config.go
  • core/mcp/auth_retry_test.go

Comment thread core/mcp/toolmanager.go
@Pratham-Mishra04
Pratham-Mishra04 force-pushed the 07-30-feat_heal_shared_mcp_connections_through_token_expiry_without_failed_calls branch from e340bd6 to 40ffed8 Compare August 8, 2026 08:43
@Pratham-Mishra04
Pratham-Mishra04 force-pushed the 07-31-feat_ui_support_for_token_exchange_mcp_auth_type branch from 01d9624 to 0c63dfc 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 `@core/mcp/auth_retry_test.go`:
- Around line 266-276: Remove the unused authRetryClientManager.resetInflight
method and update the nearby comment to describe retained inflight-record
behavior without referencing that removed helper.
🪄 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: 421f0d89-a9a4-4628-97f5-93a3ed097acb

📥 Commits

Reviewing files that changed from the base of the PR and between e340bd6 and 40ffed8.

📒 Files selected for processing (6)
  • core/mcp/auth_retry_test.go
  • core/mcp/toolmanager.go
  • framework/configstore/rdb.go
  • framework/configstore/store.go
  • framework/oauth2/sync.go
  • transports/bifrost-http/lib/config.go
🚧 Files skipped from review as they are similar to previous changes (4)
  • framework/configstore/store.go
  • framework/configstore/rdb.go
  • transports/bifrost-http/lib/config.go
  • core/mcp/toolmanager.go

Comment on lines +266 to +276
// resetInflight clears any completed inflight record so the next
// ReconnectClient call starts from a clean no-op state. Only needed by tests
// that reuse the same authRetryClientManager across multiple reconnect
// phases and require no stale op to be observable via AwaitReconnect;
// ReconnectClient's own CompareAndSwap-style replace makes this unnecessary
// for tests that just call ReconnectClient again.
func (m *authRetryClientManager) resetInflight() {
m.inflightMu.Lock()
m.inflight = nil
m.inflightMu.Unlock()
}

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.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

resetInflight is unused and fails the unused linter.

golangci-lint reports func (*authRetryClientManager).resetInflight is unused. No test in this file calls it. Remove it, or add the test that needs the clean no-op state. The comment block at lines 253-261 can keep documenting the retained-record behavior without referencing a method that does not exist.

🧹 Proposed fix: drop the dead helper
-// resetInflight clears any completed inflight record so the next
-// ReconnectClient call starts from a clean no-op state. Only needed by tests
-// that reuse the same authRetryClientManager across multiple reconnect
-// phases and require no stale op to be observable via AwaitReconnect;
-// ReconnectClient's own CompareAndSwap-style replace makes this unnecessary
-// for tests that just call ReconnectClient again.
-func (m *authRetryClientManager) resetInflight() {
-	m.inflightMu.Lock()
-	m.inflight = nil
-	m.inflightMu.Unlock()
-}
-

Also update the trailing sentence at line 260-261 to drop the resetInflight reference.

📝 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
// resetInflight clears any completed inflight record so the next
// ReconnectClient call starts from a clean no-op state. Only needed by tests
// that reuse the same authRetryClientManager across multiple reconnect
// phases and require no stale op to be observable via AwaitReconnect;
// ReconnectClient's own CompareAndSwap-style replace makes this unnecessary
// for tests that just call ReconnectClient again.
func (m *authRetryClientManager) resetInflight() {
m.inflightMu.Lock()
m.inflight = nil
m.inflightMu.Unlock()
}
🧰 Tools
🪛 golangci-lint (2.12.2)

[error] 272-272: func (*authRetryClientManager).resetInflight is unused

(unused)

🤖 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 `@core/mcp/auth_retry_test.go` around lines 266 - 276, Remove the unused
authRetryClientManager.resetInflight method and update the nearby comment to
describe retained inflight-record behavior without referencing that removed
helper.

Source: Linters/SAST tools

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, 10:06 AM UTC: Graphite rebased this pull request as part of a merge.
  • Aug 8, 10:08 AM UTC: @Pratham-Mishra04 merged this pull request with Graphite.

@Pratham-Mishra04
Pratham-Mishra04 changed the base branch from 07-31-feat_ui_support_for_token_exchange_mcp_auth_type to graphite-base/5726 August 8, 2026 10:02
@Pratham-Mishra04
Pratham-Mishra04 changed the base branch from graphite-base/5726 to dev August 8, 2026 10:05
@Pratham-Mishra04
Pratham-Mishra04 requested a review from a team as a code owner August 8, 2026 10:05
…-hint gap in budget test

authRetryClientManager.ReconnectClient cleared m.inflight to nil in the
same critical section that closed the op's done channel, unlike the
real beginExclusiveClientOp it mirrors (which deliberately leaves a
completed op in place, replacing it only lazily on the next call).
A caller that lost the race and got 'already in progress' could poll
AwaitReconnect a moment after the winner cleared inflight, observe nil,
and wrongly conclude nothing was ever in flight — the
TestExecuteTool_AuthFailureRetry_Shared_Concurrent401sJoinOneReconnect
errs[1] == nil assertion depended on winning that race. Mirror the real
CompareAndSwap-on-a-done-op replace behavior instead, and add
resetInflight for tests that need an explicit clean-slate reset.

TestExecuteTool_AuthFailureRetry_Shared_FallsBackWhenReconnectExceedsBudget
built its client state with no idempotent hint, so its 'no retry'
assertions could pass for the wrong reason (annotation fail-closed)
instead of proving the budget give-up path it's named for. Pass an
explicit idempotent hint, matching the sibling reconnect-mechanics
tests.
@Pratham-Mishra04
Pratham-Mishra04 force-pushed the 07-30-feat_heal_shared_mcp_connections_through_token_expiry_without_failed_calls branch from 40ffed8 to 8b7af9a Compare August 8, 2026 10:05
@Pratham-Mishra04
Pratham-Mishra04 merged commit b8c8414 into dev Aug 8, 2026
14 of 15 checks passed
@Pratham-Mishra04
Pratham-Mishra04 deleted the 07-30-feat_heal_shared_mcp_connections_through_token_expiry_without_failed_calls branch August 8, 2026 10:08
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