Summary
On self-hosted Executor v1.5.37, concurrent OAuth refreshes are deduplicated within one scoped Executor instance, but not across separate MCP execution stacks or sessions. With providers that rotate refresh tokens, two sessions can submit the same refresh token. The provider then rejects reuse and may revoke the entire token family.
This appears to be a cross-session gap in the concurrency fix from #367.
Environment
- Official self-hosted v1.5.37 image
- Single Executor container and single SQLite database
- Encrypted-secrets credential provider
- Several toolkit MCP endpoints consumed by multiple long-lived and short-lived MCP clients
- One Executor replica, with no database restore during the reproduction
Observed behavior
Four separate Todoist MCP connections were reauthorized. Each connection has its own DCR client and user partition; no OAuth client is shared between those connections.
For every connection:
- The initial access token worked.
- The first post-expiry refresh worked.
- Executor persisted the returned rotated refresh token and advanced
expires_at.
- A later refresh cycle failed with:
invalid_grant: refresh token reuse detected; tokens for this client/user/resource revoked
All four connections eventually reached the same state independently.
A separate self-hosted n8n MCP OAuth connection also refreshed successfully once, then later returned HTTP 401 from its refresh endpoint. A static-token MCP connection remains healthy. Notion OAuth currently continues to refresh, likely because Notion documents a one-token grace window for refresh rotation.
Why this looks cross-session
The v1.5.37 implementation creates refreshInFlight inside the Executor instance:
packages/core/sdk/src/executor.ts
A scoped Executor is built by makeScopedExecutor, and MCP server or session construction builds an execution stack containing a new scoped Executor:
packages/core/api/src/server/scoped-executor.ts
packages/core/api/src/server/mcp-build.ts
As a result, the per-connection refreshInFlight map is not shared by separate MCP execution stacks. Two stacks can both read the same stored refresh token and each believe they are the refresh winner.
This is particularly dangerous for rotating refresh tokens. Notion's own MCP documentation warns that refreshes must use a mutex or distributed lock across workers or replicas and that reuse can revoke the connection:
https://developers.notion.com/guides/mcp/build-mcp-client
Reproduction outline
- Run self-hosted Executor v1.5.37.
- Add a remote MCP integration using rotating OAuth refresh tokens, such as hosted Todoist MCP.
- Create one saved connection and expose it through a toolkit.
- Connect two or more independent MCP sessions to that toolkit.
- Let the access token expire.
- Trigger tool discovery and/or tool calls from the sessions around the same time.
- The first refresh may succeed, but a competing stack can reuse the retired refresh token.
- On a later call, observe
invalid_grant, refresh-token reuse, and required reauthorization.
The race may be easier to trigger when stale remote tool catalogs are also refreshing.
Expected behavior
- At most one refresh request per tenant, owner, subject, integration, and connection across all MCP sessions in the self-host process.
- After acquiring the lock, reload the connection and refresh-token value before deciding to refresh.
- For multi-replica deployments, use database-backed coordination or compare-and-swap token rotation rather than a process-local map.
- Waiters should use the newly persisted token instead of replaying the previous token.
Actual behavior
Refresh deduplication is scoped to one Executor instance or execution stack, so independent MCP sessions can still reuse a rotating refresh token and permanently invalidate the connection.
Additional notes
- No credentials, tokens, private URLs, or user identifiers are included here.
- The first successful refresh can make the issue appear resolved until a later expiry cycle.
- Provider error reporting varies: Todoist clearly reports token reuse, while other providers may return only 401 or 404 responses.
Summary
On self-hosted Executor v1.5.37, concurrent OAuth refreshes are deduplicated within one scoped Executor instance, but not across separate MCP execution stacks or sessions. With providers that rotate refresh tokens, two sessions can submit the same refresh token. The provider then rejects reuse and may revoke the entire token family.
This appears to be a cross-session gap in the concurrency fix from #367.
Environment
Observed behavior
Four separate Todoist MCP connections were reauthorized. Each connection has its own DCR client and user partition; no OAuth client is shared between those connections.
For every connection:
expires_at.All four connections eventually reached the same state independently.
A separate self-hosted n8n MCP OAuth connection also refreshed successfully once, then later returned HTTP 401 from its refresh endpoint. A static-token MCP connection remains healthy. Notion OAuth currently continues to refresh, likely because Notion documents a one-token grace window for refresh rotation.
Why this looks cross-session
The v1.5.37 implementation creates
refreshInFlightinside the Executor instance:packages/core/sdk/src/executor.tsA scoped Executor is built by
makeScopedExecutor, and MCP server or session construction builds an execution stack containing a new scoped Executor:packages/core/api/src/server/scoped-executor.tspackages/core/api/src/server/mcp-build.tsAs a result, the per-connection
refreshInFlightmap is not shared by separate MCP execution stacks. Two stacks can both read the same stored refresh token and each believe they are the refresh winner.This is particularly dangerous for rotating refresh tokens. Notion's own MCP documentation warns that refreshes must use a mutex or distributed lock across workers or replicas and that reuse can revoke the connection:
https://developers.notion.com/guides/mcp/build-mcp-client
Reproduction outline
invalid_grant, refresh-token reuse, and required reauthorization.The race may be easier to trigger when stale remote tool catalogs are also refreshing.
Expected behavior
Actual behavior
Refresh deduplication is scoped to one Executor instance or execution stack, so independent MCP sessions can still reuse a rotating refresh token and permanently invalidate the connection.
Additional notes