Problem
Raised by Copilot on the v2.9.0 milestone-merge PR (#2536, review comment), and confirmed in the code.
#2481 moved acquired OAuth tokens out of the OAuth state file (oauth.json) and into the secret store. The secret store is not scoped by which state file is in use:
- Its location ignores
MCP_INSPECTOR_OAUTH_STATE_PATH. It is the OS keychain by default, one per user. The file backend is MCP_INSPECTOR_SECRET_FILE, else $MCP_STORAGE_DIR/secrets.json, else ~/.mcp-inspector/secrets.json (core/auth/node/secret-store-selection.ts:126-142).
- Its entries are keyed only by server URL or issuer (
oauthSecretServerId → oauth+<encoded url>, core/auth/node/oauth-secrets.ts:78).
So two CLI/TUI profiles that point MCP_INSPECTOR_OAUTH_STATE_PATH at different state files, but connect to the same server, now share one secret-store entry. When profile B logs in, it overwrites profile A's tokens. Profile A's state file is then joined with B's credentials, so A silently acts as B's identity. Before #2481, tokens lived inside each state file, so profiles were isolated. This is a regression from #2481.
Who is affected
Only people who set MCP_INSPECTOR_OAUTH_STATE_PATH to keep separate OAuth profiles against the same server. The default single-profile setup is unaffected.
docs/cli-smoke-testing.md recommends MCP_STORAGE_DIR + MCP_INSPECTOR_OAUTH_STATE_PATH for isolation. With the default keychain backend, that setup now shares tokens with the user's real keychain entries for the same server URL.
Workaround (until fixed)
Give each profile its own secret store as well as its own state file:
export MCP_INSPECTOR_SECRET_STORE=file
export MCP_INSPECTOR_SECRET_FILE=/path/to/profile-a/secrets.json # distinct per profile
A per-profile MCP_STORAGE_DIR, with MCP_INSPECTOR_SECRET_STORE=file, also works.
Fix
Namespace secret IDs by the persistence context, for example a stable hash of the resolved state-file path when it is not the default, with a migration for existing unscoped IDs, or otherwise restore per-state-file isolation. Constraints:
- IDs must stay colon-free and must not prefix-collide. The keyring store's
deleteAllForServer parses accounts at the first colon (see the comment above oauthSecretServerId).
- The default single-profile path must keep its current IDs, or migrate them transparently, so existing users are not logged out.
Update docs/cli-smoke-testing.md (and docs/environment-variables.md) in the same change so that the documented isolation recipe is isolated again.
Acceptance
Problem
Raised by Copilot on the v2.9.0 milestone-merge PR (#2536, review comment), and confirmed in the code.
#2481 moved acquired OAuth tokens out of the OAuth state file (
oauth.json) and into the secret store. The secret store is not scoped by which state file is in use:MCP_INSPECTOR_OAUTH_STATE_PATH. It is the OS keychain by default, one per user. The file backend isMCP_INSPECTOR_SECRET_FILE, else$MCP_STORAGE_DIR/secrets.json, else~/.mcp-inspector/secrets.json(core/auth/node/secret-store-selection.ts:126-142).oauthSecretServerId→oauth+<encoded url>,core/auth/node/oauth-secrets.ts:78).So two CLI/TUI profiles that point
MCP_INSPECTOR_OAUTH_STATE_PATHat different state files, but connect to the same server, now share one secret-store entry. When profile B logs in, it overwrites profile A's tokens. Profile A's state file is then joined with B's credentials, so A silently acts as B's identity. Before #2481, tokens lived inside each state file, so profiles were isolated. This is a regression from #2481.Who is affected
Only people who set
MCP_INSPECTOR_OAUTH_STATE_PATHto keep separate OAuth profiles against the same server. The default single-profile setup is unaffected.docs/cli-smoke-testing.mdrecommendsMCP_STORAGE_DIR+MCP_INSPECTOR_OAUTH_STATE_PATHfor isolation. With the default keychain backend, that setup now shares tokens with the user's real keychain entries for the same server URL.Workaround (until fixed)
Give each profile its own secret store as well as its own state file:
A per-profile
MCP_STORAGE_DIR, withMCP_INSPECTOR_SECRET_STORE=file, also works.Fix
Namespace secret IDs by the persistence context, for example a stable hash of the resolved state-file path when it is not the default, with a migration for existing unscoped IDs, or otherwise restore per-state-file isolation. Constraints:
deleteAllForServerparses accounts at the first colon (see the comment aboveoauthSecretServerId).Update
docs/cli-smoke-testing.md(anddocs/environment-variables.md) in the same change so that the documented isolation recipe is isolated again.Acceptance
MCP_INSPECTOR_OAUTH_STATE_PATHvalues against one server keep separate tokens, on both the keyring and file backends