Repository navigation
Web: migrate auth store to shared /store API (RemoteOAuthStorage parity with TUI/CLI) #1548
Description
Follow-up split out of #1514 (Auth cleanup), completed by #1531.
#1531 removed guided auth and added connect-time OAuth, install-level CIMD, per-server OAuth clear, and keychain rehydration for TUI/CLI. The remaining piece of #1514's "migrate web client auth store to /store API" scope is still open:
- Migrate the web client's OAuth storage to the shared
/storeAPI (RemoteOAuthStorage) so it reads/writes the sameoauth.jsonas the TUI/CLI file-based store, giving web ⇄ TUI ⇄ CLI parity. - Listed explicitly as a follow-up in Guided-auth removal, OAuth smoke testing, and TUI/CLI support #1531 ("Web shared OAuth store (RemoteOAuthStorage / oauth.json parity with TUI/CLI)").
Target branch: v2/main.
Scope & context (added post-hoc — this issue is the Wave-1 home for PR #1510's web-OAuth-storage migration)
This pre-existing issue was terse and cited no files, but it stands in as the Wave-1 owner of the RemoteOAuthStorage + async-hydration work from the #1510 decomposition (see tracking #1579). Delivered by PR #1592 onto the Wave-1 rollup branch 1579-wave-1 (reference implementation: #1510 head 33fac3f). The concrete scope:
Behavior
- Web OAuth store migrates from
BrowserOAuthStorage(sessionStorage) to backend-backedRemoteOAuthStorage(~/.mcp-inspector/storage/oauth.json), shared with TUI/CLI on the same host. - Introduces a single async hydration gate (
ready()/getHydrationError()); all post-redirect reads and all saves await it. Clears run hydration-safe (defer until hydrated, never clobber the on-disk blob). getCodeVerifier/getServerMetadatabecome async across the interface, propagated through the provider and consumers.
Files changed (delivered on 1579-wave-1)
core/auth/storage.ts—ready()+ async hydration gate; asyncgetCodeVerifier/getServerMetadatasignaturescore/auth/oauth-storage.ts—OAuthStorageBasehydration + hydration-safeclear*core/auth/providers.ts— asynccodeVerifier/getServerMetadatacore/auth/store.ts—skipHydration+normalizeServerUrlkeying + raw-key migrationcore/auth/connection-state.ts— awaitsgetServerMetadata(this is feat(web/apps): spec-conformant Apps host + CLI/deep-link path for programmatic review #1510'score/auth/state-machine.ts, renamed on current v2/main)core/auth/node/storage-node.ts— honorsMCP_INSPECTOR_OAUTH_STATE_PATHcore/storage/adapters/remote-storage.ts—keepalivePOSTs + surfaces persist failuresclients/web/src/lib/environmentFactory.ts— the load-bearing swap: wires web toRemoteOAuthStoragevia the memoizedgetRemoteOAuthStorage(...)accessorclients/web/src/lib/remoteOAuthStorage.ts— new memoizedRemoteOAuthStorageaccessorclients/web/src/App.tsx— async hydration in the OAuth-callback flow- Tests:
oauth-storage/storage-browser/storage-remote/oauthManager(async mocks) + integrationauth/node/storageandstorage/adapters
Verification: exhaustive @claude review to clean, coverage gate ≥90 on all four dims for the gated files, and an independent smoke/audit (backend round-trip through /api/storage/oauth, oauth.json shape matches TUI/CLI, clear-before-hydration preserves sibling tokens) — see the audit on this issue and the rollup verification in #1600.
Activity
cliffhall commentedon Jul 1, 2026
Heads-up: PR #1510 contains a complete reference implementation of this issue's scope (plus extras), now decomposed under tracking issue #1579 with this issue folded in as the OAuth-storage foundation item.
Reference implementation at
33fac3f:OAuthStorageBaseasync hydration (hydrated/ready()/getHydrationError()), getters await hydrationnormalizeServerUrl()keying (new URL().href) with raw-key fallbackkeepalive: trueon persist (survives the OAuth redirect), persist-error surfacingcodeVerifier()/getServerMetadata()go asyncMCP_INSPECTOR_OAUTH_STATE_PATHoverrideNote the CLI stored-auth suite (#1575) and deep-link auto-connect (#1576) depend on the
oauth.jsonformat and URL normalization established here.cliffhall commentedon Jul 2, 2026
Smoke test & AGENTS.md audit — PR #1592
Audited at PR head
588fc3f8(branchv2/1548-web-remote-oauth-storage), freshnpm install. This is a store/core/lib wiring change (auth storage migrated toRemoteOAuthStorage+ async hydration); there is no essential visual surface — the OAuth flow UI is unchanged, so Playwright/browser drive was not run.1. Smoke test — method + real output
Primary proof: the integration + unit suites. The RemoteOAuthStorage/RemoteStorageAdapter and the async-hydration
OAuthStorageBaseare exercised end-to-end against a real Hono backend and mocked-fetch paths.clients/webnpm run test:integration→ 38 files / 801 tests passed (the lone "1 error" is a known vitest teardown flake —EnvironmentTeardownError: Closing rpc while "onUserConsoleLog" was pending— not a test failure).Additional throwaway proof (custom script,
/tmp, not committed). Drives the realcreateRemoteAppHono backend +RemoteOAuthStorageover HTTP to prove the round-trip, on-diskoauth.jsonshape, and the pre-hydration clear guard. Real captured output:This confirms: (a) a token saved through
RemoteOAuthStorageround-trips throughPOST/GET /api/storage/oauthand is readable by a fresh instance (post-redirect page-reload semantics) only after hydration; (b) it persists tooauth.jsonin the exact shape/path the TUI/CLI read (~/.mcp-inspector/storage/oauth.json); (c) aclearTokens(A)issued before hydration lands is deferred and merges onto the real persisted blob — A's token is cleared while sibling B's token survives (no whole-blob clobber).2. AGENTS.md compliance audit
Mantine — N/A for the changed surface. Every changed file is core/store/lib wiring. The one React file (
App.tsx) changed only wiring: swapgetBrowserOAuthStorage→getRemoteOAuthStorage, auseMemo, and passing anoauthStorageprop toclearServerOAuthState. No JSX/layout/style changes, no inline styles, no raw hex/rgba.TypeScript — clean. No
anytypes (the onlyanytoken in the source diff is prose in a JSDoc comment: "Resolves once any async hydration…"), nots-ignore/ts-expect-error/eslint-disable, no newv8 ignore. The async migration is fully typed:OAuthStoragegainedready(): Promise<void>and madegetCodeVerifier/getServerMetadatareturnPromise<…>;BaseOAuthClientProvider.codeVerifier()/getServerMetadata()becameasync; every caller (connection-state.ts,ema/idpOidc.ts) was updated toawait.eslint .passed with no errors;tsc -p tsconfig.app.json --noEmitexited 0.Coverage —
clients/webnpm run test:coverage: PASSED (231 files / 3265 tests, per-file gate ≥90 on all four dims). Key touched files (S / B / F / L):core/auth/oauth-storage.tscore/auth/store.tscore/auth/storage.tscore/auth/providers.tscore/auth/utils.tscore/auth/connection-state.tscore/auth/ema/idpOidc.tscore/auth/node/storage-node.tscore/storage/adapters/remote-storage.tsclients/web/src/utils/clearServerOAuthState.tsAll ≥ 90 on every dimension.
Verdict
✅ PASS. Integration + unit suites green; RemoteOAuthStorage round-trips through the backend to
oauth.jsonwith hydration-safe reads and a pre-hydration clear that does not clobber sibling tokens; TypeScript clean (noany/suppressions, async signatures fully typed); lint + typecheck pass; coverage gate passes with all key touched files ≥90. Only advisory: thesrc/libwiring files sit outside the coverage include globs.Throwaway proof script (
/tmp, not committed) — run viavitest --project integration🤖 Smoke test + AGENTS.md audit performed by Claude Code.
cliffhall commentedon Jul 2, 2026
🔗 Implemented by PR #1592. Its code is already integrated into the Wave 1 rollup branch
1579-wave-1(see rollup PR #1601 / verification #1600). #1592 currently targetsv2/mainand will auto-show as Merged once the rollup lands inv2/main. The Development panel doesn't link it now because the closing keyword is on a non-default base branch.feat(web): migrate OAuth store to shared /store API (RemoteOAuthStora…