Skip to content

CLI stored-auth flags (--use-stored-auth, --list-stored-auth, --wait-for-auth) miss tokens stored under byIssuer #2517

Closed

Description

@cliffhall

Problem

The CLI's stored-auth flags can't see a token the shared OAuth store actually holds:

  • --use-stored-auth exits 3 with no_stored_token, and its message lists the very URL it failed to match under "Stored keys".
  • --list-stored-auth returns "storedServerUrls": [].
  • --wait-for-auth goes through the same lookup (findStoredToken), so going by the code it waits until it times out even after the token lands. I haven't run this one.

The lookups in clients/cli/src/cli.ts read the token at the server level: state.tokens?.access_token / state.tokens?.refresh_token (on v2/main: L325 findStoredToken, L388, L1010 --list-stored-auth, L1120–1121 --use-stored-auth). Since the OAuth store was re-keyed per issuer (#1625), acquired tokens are stored at servers[url].byIssuer[activeIssuer].tokens, so none of these lookups ever matches. --stored-auth-only is unaffected because it goes through the normal auth provider path, which does read byIssuer.

Found while smoke testing #2482, where it reproduced identically on v2/main (30a9a897) and on the PR head (fc55e11a). So it is not caused by that PR. The PR does change where the tokens live, since they move to the secret store and are joined back on read, so a fix should read the joined view.

Reproduce

  1. Start an OAuth test server: node test-servers/build/server-composable.js --config test-servers/configs/oauth-revocation-http.json (:8083).
  2. With an isolated MCP_STORAGE_DIR, complete an interactive CLI login: mcp-inspector --cli --server-url http://localhost:8083/mcp --method tools/list.
  3. Check the token is there: --stored-auth-only --method tools/list succeeds, and oauth.json shows servers["http://localhost:8083/mcp"].byIssuer["http://localhost:8083"].tokens.
  4. mcp-inspector --cli --list-stored-auth prints storedServerUrls: [].
  5. mcp-inspector --cli --server-url http://localhost:8083/mcp --use-stored-auth --method tools/list exits 3 with {"error":{"code":"no_stored_token",…"Stored keys: http://localhost:8083/mcp."}}.

Expected

  • --list-stored-auth lists the URL.
  • --use-stored-auth refreshes (or injects) the active issuer's token and succeeds.
  • --wait-for-auth returns as soon as the token lands.
  • The refresh write-back persists the rotated tokens under the active issuer.

The existing stored-auth tests should cover the byIssuer shape, so this can't regress silently again.

Activity

  1. added this to the v2.9.0 milestone on Sep 27, 2026
  2. added
    bugSomething isn't working
    v2Issues and PRs for v2
    on Sep 27, 2026
  3. cliffhall commented on Sep 27, 2026

    @cliffhall
    MemberAuthor

    Priority: Medium (8). Severity 3: the CLI's stored-auth handoff is broken, and the only workaround is partial (--stored-auth-only works for reuse, but there is no refresh or wait). Urgency 3: wanted this milestone, since #2482 moves where tokens live and the fix should target the joined view. Bonuses: +1 bug, +1 milestoned.

  4. Ben-Home commented on Sep 29, 2026

    @Ben-Home
  5. modified the milestones: v2.9.0, v2.10.0 on Sep 30, 2026
  6. felixlheureux commented on Oct 2, 2026

    @felixlheureux

    Additional real-world reproduction from VibeEasy development acceptance against published Inspector 2.9.0:

    • Fresh Inspector-managed OAuth completed successfully.
    • Separate --stored-auth-only processes negotiated MCP 2026-07-28, discovered 26 tools, and successfully invoked projects.list.
    • --list-stored-auth still returned no server URLs.
    • --use-stored-auth exited 3 with no_stored_token.

    The published 2.9.0 bundle and current main both show the same mismatch: shared OAuth storage writes issuer-bound credentials under servers[serverUrl].byIssuer[issuer], while CLI stored-auth helpers still read legacy top-level state.tokens / state.clientInformation.

    The fix should reuse the existing issuer-aware storage semantics rather than flattening credentials:

    1. Resolve the matching server entry by normalized URL.
    2. Resolve tokens and client information from the same issuer slot.
    3. Prefer the active issuer when the storage model has one; never select an arbitrary issuer.
    4. Preserve legacy top-level fallback for pre-issuer state.
    5. Refresh must use issuer-matched client information/metadata and persist rotated tokens back to that issuer slot.
    6. --wait-for-auth and --list-stored-auth must use the same resolver.

    Regression coverage requested:

    • issuer-scoped credentials appear in --list-stored-auth;
    • --use-stored-auth reuses issuer-scoped access credentials;
    • refresh uses matching issuer-specific client info and persists rotation correctly;
    • --wait-for-auth recognizes newly persisted issuer-scoped credentials;
    • legacy credentials still work;
    • multiple issuers, missing credentials, and refresh failures remain safe/explicit.

    VibeEasy did not inject bearer tokens, inspect raw OAuth files, or modify its Auth implementation to get around this. The acceptance runner remains blocked on this upstream behavior.

  7. self-assigned this
    on Oct 5, 2026
  8. added a commit that references this issue on Oct 5, 2026
  9. cliffhall commented on Oct 5, 2026

    @cliffhall
    MemberAuthor

    Fixed by #2572, merged into v2/main.

  10. added a commit that references this issue on Oct 8, 2026
    1c0d181
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingv2Issues and PRs for v2

Type

No type

Projects

No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions