Removed Zustand storage, cleanup up auth storage IO interfaces, implementations, and usage.
This commit is contained in:
parent
512f45d357
commit
3c65bdb4c6
10 changed files with 863 additions and 1008 deletions
|
|
@ -109,7 +109,7 @@ Until **client profiles** (see below) land, install-level client config — star
|
|||
|
||||
**Do not** put IdP credentials in the OAuth store or per-server `mcp.json`. **Do not** use environment variables for IdP config — all clients read/write the same `client.json` file.
|
||||
|
||||
**Runtime auth state** (standard OAuth tokens, PKCE, **and** EMA runtime state: cached IdP ID Token / refresh token, leg-1 in-flight PKCE under `ema-idp:{issuer}`, and per-server resource tokens tagged `enterpriseManaged: true` when minted via EMA legs 2–3) uses the existing **`OAuthStorage`** interface (`core/auth/storage.ts`) — whatever adapter each client already passes to `InspectorClient`. ID-JAG is **not** cached — legs 2–3 re-mint on each connect or 401 refresh. EMA does **not** mandate a specific backing store; it extends the serialized auth state that adapter already persists.
|
||||
**Runtime auth state** (standard OAuth tokens, PKCE, **and** EMA runtime state: cached IdP ID Token / refresh token, leg-1 in-flight PKCE under `ema-idp:{issuer}`, and per-server resource tokens tagged `enterpriseManaged: true` when minted via EMA legs 2–3) uses the **`OAuthStorage`** interface (`core/auth/storage.ts`) — whichever implementation each client passes to `InspectorClient` (`NodeOAuthStorage`, `RemoteOAuthStorage`, etc.). ID-JAG is **not** cached — legs 2–3 re-mint on each connect or 401 refresh.
|
||||
|
||||
Web uses shared file-backed OAuth via `RemoteOAuthStorage` → `/api/storage/oauth` (same `oauth.json` as CLI/TUI). Existing **sessionStorage** OAuth blobs from before that wiring are not migrated automatically — users start fresh or re-authorize once.
|
||||
|
||||
|
|
@ -215,7 +215,7 @@ interface OAuthStoreState {
|
|||
}
|
||||
```
|
||||
|
||||
Extend `OAuthStorage` / store methods accordingly. IdP **credentials** stay in `client.json`; only runtime tokens belong in `idpSessions`. Per-server `ServerOAuthState` may include `enterpriseManaged: true` when EMA legs 2–3 persist resource tokens (`saveTokens(..., { enterpriseManaged: true })`). **Sign out** clears `idpSessions` for the configured issuer and removes tagged EMA resource entries from the shared OAuth blob — standard OAuth server entries are not cleared. All adapters (`BrowserOAuthStorage`, `NodeOAuthStorage`, `RemoteOAuthStorage`) serialize the same extended shape through their existing backends.
|
||||
Extend `OAuthStorage` / store methods accordingly. IdP **credentials** stay in `client.json`; only runtime tokens belong in `idpSessions`. Per-server `ServerOAuthState` may include `enterpriseManaged: true` when EMA legs 2–3 persist resource tokens (`saveTokens(..., { enterpriseManaged: true })`). **Sign out** clears `idpSessions` for the configured issuer and removes tagged EMA resource entries from the shared OAuth blob — standard OAuth server entries are not cleared. All implementations (`BrowserOAuthStorage`, `NodeOAuthStorage`, `RemoteOAuthStorage`) extend `OAuthStorageBase` and serialize the same shape through their persist backends (`core/auth/oauth-persist.ts`).
|
||||
|
||||
#### Later: client profiles
|
||||
|
||||
|
|
@ -298,7 +298,7 @@ Install-level IdP credentials are edited in **Client Settings**, separate from p
|
|||
2. Clears leg-1 in-flight state at `servers["ema-idp:{issuer}"]`
|
||||
3. Calls `OAuthStorage.clearEnterpriseManagedResourceServers()` — scans the shared OAuth blob and removes `servers[url]` entries where `enterpriseManaged === true` (set when EMA legs 2–3 saved tokens via `saveTokens(url, tokens, { enterpriseManaged: true })` in `emaFlow.ts` / `EmaTransportOAuthProvider`). Standard OAuth entries in the same blob are **not** cleared. Untagged legacy EMA tokens (saved before tagging landed) are not removed until the next EMA connect re-saves with the tag.
|
||||
The next connect to a cleared EMA server has no cached access token, so a **401** triggers leg 1 (IdP login) via `authenticate()`.
|
||||
- **Web OAuth store singleton:** web uses `getWebRemoteOAuthStorage()` (`clients/web/src/lib/remoteOAuthStorage.ts`) — memoized `RemoteOAuthStorage` backed by `/api/storage/oauth` — so Client Settings sign-out, EMA IdP session, connect, and per-server clear all mutate the same in-memory Zustand view as the active `InspectorClient`.
|
||||
- **Web OAuth store singleton:** web uses `getWebRemoteOAuthStorage()` (`clients/web/src/lib/remoteOAuthStorage.ts`) — memoized `RemoteOAuthStorage` backed by `/api/storage/oauth` — so Client Settings sign-out, EMA IdP session, connect, and per-server clear all mutate the same in-memory `OAuthStorageBase` view as the active `InspectorClient`.
|
||||
|
||||
**Future (not implemented):** explicit **Sign out** may additionally invoke the IdP's OIDC **end-session** / logout endpoint (RP-initiated logout) so the IdP SSO cookie is cleared — not just inspector-local IdP/resource token state. Today sign-out is **local-only** (clear `idpSessions`, tagged EMA resource entries, and leg-1 PKCE); the IdP may still treat the browser as signed in and skip the login prompt on the next authorize redirect until the IdP session expires or the user signs out at the IdP.
|
||||
|
||||
|
|
@ -416,7 +416,7 @@ Inspector touchpoints to extend:
|
|||
- `core/client/` (or `core/storage/`) — `loadClientConfig` / `saveClientConfig`; `NodeClientStorage`; remote adapter for `/api/storage/client`
|
||||
- `core/auth/utils.ts` — `parseHttpUrl` (issuer and EMA URL validation)
|
||||
- `core/auth/connection-state.ts` — `buildOAuthConnectionState`, `OAuthConnectionState`
|
||||
- `core/auth/storage.ts` + `core/auth/store.ts` — `OAuthStorage` / `OAuthStoreState`: store-root `idpSessions`; `ServerOAuthState.enterpriseManaged` tag; `SaveTokensOptions`; `clearEnterpriseManagedResourceServers()`; all adapters pick up the extended shape automatically
|
||||
- `core/auth/storage.ts` + `core/auth/store.ts` + `core/auth/oauth-storage.ts` + `core/auth/oauth-persist.ts` — `OAuthStorage` / `OAuthMemoryStore`: store-root `idpSessions`; `ServerOAuthState.enterpriseManaged` tag; async getters with auto-load; setters auto-persist; legacy `{ state, version }` envelope promoted on read, plain JSON on write
|
||||
- `core/auth/ema/` — `idpOidc.ts` (leg 1), `wire.ts` (legs 2–3), `emaFlow.ts` (orchestration + tagged `saveTokens`), `transportProvider.ts` (401 re-auth + tagged `saveTokens`), `resourceContext.ts`, `idpSession.ts` (`getEmaIdpLoginState`, `clearEmaIdpSession`), `jwt.ts`, `storage.ts`, `constants.ts`, `clientConfigError.ts` (`EmaClientNotConfiguredError`)
|
||||
- `core/mcp/oauthManager.ts` — branch on `enterpriseManaged`; EMA connect via `emaFlow`; `getOAuthState()`; `createOAuthProvider()` returns `EmaTransportOAuthProvider` for EMA servers (401 re-auth); standard OAuth unchanged
|
||||
- `core/mcp/inspectorClient.ts` — declare EMA extension in `initialize` capabilities when `enterpriseManaged`; `getOAuthState()`
|
||||
|
|
@ -503,7 +503,7 @@ Design decisions for EMA are complete. Remaining work is the phased plan and che
|
|||
|
||||
- [x] **Manual staging validation** — full EMA connect against live xaa.dev (see §Staging validation). Confirms legs 1–3 and web UX outside CI.
|
||||
- [x] **Automated integration tests** — mock IdP + mock resource AS + composable protected-resource server (see §Phase 3b test plan). Live xaa.dev is **not** required for CI.
|
||||
- [ ] *(optional)* `RemoteOAuthStorage` EMA E2E variant — same happy path via `/api/storage/oauth` (shared-storage follow-up).
|
||||
- [ ] *(optional)* `RemoteOAuthStorage` EMA E2E variant — same happy path via `/api/storage/oauth` (standard OAuth remote E2E exists; EMA-specific variant optional).
|
||||
- [ ] *(optional)* 401 re-auth stretch — invalidate resource token, assert legs 2–3 re-run.
|
||||
|
||||
### Phase 4 — Other clients
|
||||
|
|
@ -514,8 +514,8 @@ Design decisions for EMA are complete. Remaining work is the phased plan and che
|
|||
|
||||
### Later
|
||||
|
||||
- [ ] **Remove Zustand from OAuth persistence** — direct read/write of OAuth blob; see §Follow-up work
|
||||
- [x] **Web shared OAuth store** — `RemoteOAuthStorage` / `oauth.json` for web + CLI + TUI parity (#1548); see §Follow-up work
|
||||
- [x] **Remove Zustand from OAuth persistence** — `OAuthStorageBase` + persist backends (#1549); see §OAuth persistence
|
||||
- [x] **Web shared OAuth store** — `RemoteOAuthStorage` / `oauth.json` for web + CLI + TUI parity (#1548); see §Shared file-backed OAuth state
|
||||
- [ ] Client profile persistence (migrate from `client.json`; may extend or replace web Client Settings)
|
||||
- [ ] Optional: adopt `@modelcontextprotocol/client` v2 Layer-2 helpers for legs 2–3 (replace `wire.ts`) or full v2 transport for EMA
|
||||
- [ ] Optional: IdP **end-session** / RP-initiated logout on explicit **Sign out** (today sign-out clears inspector-local IdP session and tagged EMA resource tokens only; IdP browser SSO may remain active)
|
||||
|
|
@ -553,14 +553,21 @@ Registration on xaa.dev: composable test server registered as a **resource serve
|
|||
|
||||
These items came out of EMA staging and apply beyond EMA. They are **not** required to ship EMA on web but should be tracked.
|
||||
|
||||
### Remove Zustand from OAuth persistence
|
||||
### OAuth persistence (#1549 — done)
|
||||
|
||||
Today `OAuthStorage` is backed by a **Zustand** `persist` store (`core/auth/store.ts`) with adapters for sessionStorage (`BrowserOAuthStorage`), file (`NodeOAuthStorage` via `file-storage.ts`), and HTTP (`RemoteOAuthStorage` via `remote-storage.ts`). The Zustand layer wraps a simple `{ servers, idpSessions }` blob and adds:
|
||||
`OAuthStorage` is implemented by **`OAuthStorageBase`** (`core/auth/oauth-storage.ts`) backed by **`OAuthMemoryStore`** (`core/auth/store.ts`) and an explicit **`OAuthPersistBackend`** (`core/auth/oauth-persist.ts`):
|
||||
|
||||
- `{ state, version }` envelope on file/remote adapters (see [Servers file](v2_servers_file.md) — intentionally avoided for `mcp.json`)
|
||||
- A second in-memory copy and async persist semantics that complicate sign-out, callback rebuild, and multi-consumer sharing (web uses `getWebRemoteOAuthStorage()`; legacy `getBrowserOAuthStorage()` existed partly to paper over this for sessionStorage)
|
||||
| Backend | Class | Where |
|
||||
| ------- | ----- | ----- |
|
||||
| File | `NodeOAuthStorage` | CLI, TUI, default `~/.mcp-inspector/storage/oauth.json` |
|
||||
| Remote HTTP | `RemoteOAuthStorage` | Web → `GET/POST/DELETE /api/storage/oauth` on the Hono backend |
|
||||
| Session | `BrowserOAuthStorage` | Reference/tests only; v2 web uses shared file-backed remote store |
|
||||
|
||||
**Direction:** refactor `OAuthStorage` implementations to read/write the serialized shape **directly** (file I/O, `/api/storage/oauth`, or in-memory for tests) without Zustand. Keep the `OAuthStorage` interface stable for `InspectorClient` / `OAuthManager`.
|
||||
**On disk / wire:** writes plain JSON `{ servers, idpSessions }`. **Reads** still accept legacy blobs wrapped as `{ state: { servers, idpSessions }, version }` (produced by the old Zustand `persist` middleware) and promote the inner payload — migrate-on-write on the next save.
|
||||
|
||||
**API shape:** all getters are async (`ensureLoaded()` internally); setters `await` persist. `load()` is optional preload (OAuth callback fail-fast on web), not required before reads. `BaseOAuthClientProvider.prepareForAuth()` caches scope for the SDK’s sync `clientMetadata.scope`.
|
||||
|
||||
**Removed:** Zustand dependency, `core/storage/adapters/*`, public `getOAuthStore()`.
|
||||
|
||||
### Shared file-backed OAuth state (web + CLI + TUI)
|
||||
|
||||
|
|
@ -639,7 +646,7 @@ Shared helper `minimalOAuthAsMetadata()` satisfies SDK OAuth metadata schema. `c
|
|||
|
||||
Runs under `integration` project; `test-servers:build` prerequisite (existing). Transport: `streamable-http` only (EMA is HTTP-only).
|
||||
|
||||
**Storage variant (optional — not implemented):** same flow with `RemoteOAuthStorage` + `createRemoteApp` tmp dir — proves EMA tokens persist via `/api/storage/oauth` (feeds shared-storage follow-up).
|
||||
**Storage variant (optional — not implemented):** same flow with `RemoteOAuthStorage` + `createRemoteApp` tmp dir — proves EMA tokens persist via `/api/storage/oauth` (standard remote OAuth E2E already covers the storage API).
|
||||
|
||||
#### Layer 4 — Protected-resource discovery — **partial**
|
||||
|
||||
|
|
@ -658,7 +665,7 @@ Existing coverage: `xaa-ema-http.json` config load, `ExternalAccessTokenValidato
|
|||
1. ~~Layer 1 (`wire` / `emaFlow` unit tests)~~ — **done**
|
||||
2. ~~Layer 2 mock servers helper~~ — **done**
|
||||
3. ~~Layer 3 single happy-path E2E (`streamable-http`)~~ — **done**
|
||||
4. Layer 3 storage variant with `RemoteOAuthStorage` — optional, aligns with shared-storage refactor.
|
||||
4. Layer 3 storage variant with `RemoteOAuthStorage` — optional EMA-specific EMA E2E (shared storage already covered by standard remote OAuth tests).
|
||||
5. 401 re-auth case — connect, invalidate resource token, assert legs 2–3 re-run (stretch).
|
||||
|
||||
### Done criteria for Phase 3b checklist
|
||||
|
|
|
|||
|
|
@ -199,7 +199,7 @@ Document manual scenarios in [v2_auth_smoke_testing.md](v2_auth_smoke_testing.md
|
|||
| Upgrade | Root and client `package.json`; import paths v1 → `@modelcontextprotocol/client` |
|
||||
| Callback | `clients/web/src/App.tsx`, `core/auth/node/oauth-callback-server.ts`, `core/mcp/oauthManager.ts`, `core/mcp/inspectorClient.ts` |
|
||||
| Provider | `core/auth/providers.ts`, `core/auth/browser/providers.ts` — v2 hooks: `invalidateCredentials`, issuer-keyed storage, `application_type` |
|
||||
| Storage | `core/auth/store.ts`, `core/auth/oauth-storage.ts`, `core/mcp/remote/node/remoteOAuthStorage.ts` (if landed) |
|
||||
| Storage | `core/auth/store.ts`, `core/auth/oauth-storage.ts`, `core/auth/oauth-persist.ts`, `core/auth/remote/storage-remote.ts`, `clients/web/src/lib/remoteOAuthStorage.ts` |
|
||||
| Web remote (unchanged on v2) | `core/mcp/remote/remoteClientTransport.ts`, `core/mcp/node/authChallengeFetch.ts`, `core/mcp/remote/node/server.ts` |
|
||||
| TUI/CLI | [Mid-session § TUI and CLI](v2_auth_mid_session.md#tui-and-cli-implementation): `clients/tui/src/App.tsx`; `clients/cli/src/cliOAuth.ts`; provider wiring in `core/mcp/inspectorClient.ts` |
|
||||
| Mid-session overlap | `core/auth/challenge.ts`, `core/mcp/oauthManager.ts` (`handleAuthChallenge`, `saveScope`) |
|
||||
|
|
|
|||
|
|
@ -536,13 +536,13 @@ On each MCP request, after bearer validation:
|
|||
| [v2_auth_hardening.md](v2_auth_hardening.md) | Connect-time SEPs; v2 SDK upgrade; direct transport silent retry delegation |
|
||||
| [v2_auth_ema.md](v2_auth_ema.md) | EMA legs 2–3 re-mint; scope resolution; no resource-OAuth fallback |
|
||||
| [v2_auth_smoke_testing.md](v2_auth_smoke_testing.md) | Manual OAuth and mid-session validation procedures |
|
||||
| [v2_storage.md](v2_storage.md) | Target: shared `oauth.json` via `RemoteOAuthStorage` on web |
|
||||
| [v2_storage.md](v2_storage.md) | Shared `oauth.json` via `RemoteOAuthStorage` on web (implemented) |
|
||||
|
||||
---
|
||||
|
||||
## Future work
|
||||
|
||||
- **Web default `RemoteOAuthStorage`** — shared `oauth.json` with TUI/CLI; optional `navigator.locks` for silent refresh single-flight across browser tabs.
|
||||
- **Optional `navigator.locks`** on shared `RemoteOAuthStorage` — single-flight silent refresh across browser tabs (see [EMA §Shared storage](v2_auth_ema.md)).
|
||||
- **v2 SDK transport upgrade** — delegate direct streamable HTTP silent 401/403 + SEP-2350 union to SDK; remove `mcpAuth` / client intercept shims where redundant.
|
||||
- **Server-side token refresh** on the node using `RemoteAuthState.oauthClient` + `refresh_token` (browser owns refresh today).
|
||||
- **Connection Info** — display effective vs pending scopes.
|
||||
|
|
|
|||
|
|
@ -136,7 +136,7 @@ Per-server OAuth fields live on the same entry (lifted to `InspectorServerSettin
|
|||
|
||||
### Notes
|
||||
|
||||
- First connect performs discovery + DCR + authorization code + PKCE. Tokens persist in `~/.mcp-inspector/storage/oauth.json` (TUI/CLI path) or browser session storage (web).
|
||||
- First connect performs discovery + DCR + authorization code + PKCE. Tokens persist in `~/.mcp-inspector/storage/oauth.json` (CLI/TUI direct file, web via `RemoteOAuthStorage` → same file on the local backend).
|
||||
- Reconnect should reuse stored DCR `client_id` unless storage was cleared.
|
||||
|
||||
---
|
||||
|
|
@ -847,7 +847,7 @@ Session from C1 must have **`mcp tools:read` only**. If step-up does not prompt,
|
|||
|
||||
## Known gaps (Inspector)
|
||||
|
||||
**Mid-session auth** is implemented for web (remote transport), TUI, and CLI — see [Mid-session authorization](v2_auth_mid_session.md). **Remaining:** shared `RemoteOAuthStorage` on web, optional idle SSE E2E, v2 SDK transport upgrade for direct silent retry.
|
||||
**Mid-session auth** is implemented for web (remote transport), TUI, and CLI — see [Mid-session authorization](v2_auth_mid_session.md). **Remaining:** optional idle SSE E2E, v2 SDK transport upgrade for direct silent retry.
|
||||
|
||||
See **[Auth hardening (MCP 2026-07-28)](v2_auth_hardening.md)** for connect-time OAuth hardening (SEP-2468, SEP-837, SEP-2352, SEP-2207, SEP-2350, SEP-2351) and the v2 SDK upgrade strategy.
|
||||
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ This document describes how those clients are built, wired, and tested today, an
|
|||
|
||||
- **CLI v2 sessions** (connect once, many subcommands) — tracked separately in [#1432](https://github.com/modelcontextprotocol/inspector/issues/1432).
|
||||
- **npm workspaces** — v2 uses a fat root package plus per-client `package.json` for dev dependencies; the launcher resolves sibling `build/` outputs via relative paths, not workspace hoisting.
|
||||
- _Why not workspaces:_ `core/` is consumed by **bundling** — a Vite alias for the browser, tsup inlining for the Node clients — not by symlinked package resolution, so workspaces' main benefit (cross-package linking) does not apply. Each client also pins `react` / `zustand` / `@modelcontextprotocol/sdk` to its own `node_modules` (see `vitest.shared.mts`) to avoid dual-package-instance hazards, which hoisting works against. And the published `@modelcontextprotocol/inspector` is a single flat fat package that workspaces would complicate rather than simplify.
|
||||
- _Why not workspaces:_ `core/` is consumed by **bundling** — a Vite alias for the browser, tsup inlining for the Node clients — not by symlinked package resolution, so workspaces' main benefit (cross-package linking) does not apply. Each client also pins `react` / `@modelcontextprotocol/sdk` to its own `node_modules` (see `vitest.shared.mts`) to avoid dual-package-instance hazards, which hoisting works against. And the published `@modelcontextprotocol/inspector` is a single flat fat package that workspaces would complicate rather than simplify.
|
||||
- _Cost (from-source dev only):_ there is no hoisting, so each client keeps its own `node_modules`. A root `postinstall` (`scripts/install-clients.mjs`) cascades `npm install` into every client, so a single `npm install` at the repo root populates them all — re-run it after a pull that changes a client's dependencies. The cascade no-ops outside a source checkout (it exits early when running from `node_modules`, and the published tarball ships only each client's `build/`, no client `package.json`), so end users of the published package are unaffected. Set `INSPECTOR_SKIP_CLIENT_INSTALL=1` to skip the cascade (e.g. CI that installs each client itself).
|
||||
- **Per-client coverage gates** — `core/` coverage stays on the web suite; CLI/TUI source gates are follow-up work (see [Known gaps](#known-gaps)).
|
||||
- **Catalog CRUD in TUI** — TUI loads and connects; persistent catalog editing remains web-first today.
|
||||
|
|
|
|||
|
|
@ -302,7 +302,7 @@ The Inspector v2 currently depends on `@modelcontextprotocol/sdk@1.29.0`. SDK v2
|
|||
|
||||
## 9. Impact on MCP Inspector V2 (`v2/main`)
|
||||
|
||||
Grounded in the current branch architecture: `InspectorClient` (core/mcp/inspectorClient.ts) wrapping SDK 1.29 with typed events → state stores → React hooks; browser transport remoted through the Hono backend (`core/mcp/remote/`); zustand-persisted OAuth store; capability-gated tabs.
|
||||
Grounded in the current branch architecture: `InspectorClient` (core/mcp/inspectorClient.ts) wrapping SDK 1.29 with typed events → `core/mcp/state` stores → React hooks; browser transport remoted through the Hono backend (`core/mcp/remote/`); file-backed OAuth via `OAuthStorageBase` (`oauth.json`); capability-gated tabs.
|
||||
|
||||
### 9.1 Connection model and state management
|
||||
|
||||
|
|
@ -331,7 +331,7 @@ This is the deepest change. Today `InspectorClient` and the UI assume: connect
|
|||
|
||||
### 9.4 Auth and the OAuth store
|
||||
|
||||
- **Re-key the zustand OAuth store** (`core/auth/store.ts`) from per-server to per-`(server, issuer)`: stamp `issuer` on stored `clientInformation`/`tokens`, detect issuer changes against freshly fetched PRM/AS metadata on every flow, invalidate + re-register (DCR) / error (static) / continue (CIMD) accordingly. SDK v2 handles the flow logic but expects providers to round-trip issuer-stamped objects and optionally implement `discoveryState()`/`saveDiscoveryState()`.
|
||||
- **Re-key OAuth storage** (`core/auth/store.ts` / `OAuthStorage`) from per-server to per-`(server, issuer)`: stamp `issuer` on stored `clientInformation`/`tokens`, detect issuer changes against freshly fetched PRM/AS metadata on every flow, invalidate + re-register (DCR) / error (static) / continue (CIMD) accordingly. SDK v2 handles the flow logic but expects providers to round-trip issuer-stamped objects and optionally implement `discoveryState()`/`saveDiscoveryState()`.
|
||||
- **Persist the requested-scope set** per (server, issuer) and show the step-up union computation in the auth visualizations — this is exactly the kind of thing Inspector users will want to see when debugging 403 loops. Surface the `onInsufficientScope` policy as a setting.
|
||||
- **DCR panel:** show the `application_type` sent (Inspector = `"native"`), render registration rejections meaningfully, and reflect DCR's deprecated-in-favor-of-CIMD status (Inspector v2 already supports CIMD registration kind — good position).
|
||||
- **401 handling under negotiation:** unwrap `EraNegotiationFailed` → `UnauthorizedError` in `oauthManager.ts`; switch to `finishAuth(callbackParams)` whole-params form (RFC 9207 iss validation happens inside; mismatches throw `IssuerMismatchError` — a new error state to display).
|
||||
|
|
|
|||
|
|
@ -45,9 +45,8 @@
|
|||
* **Enterprise-managed authorization (EMA / XAA)** — see [EMA / XAA](v2_auth_ema.md)
|
||||
* **Mid-session authorization** (401 after connect: token refresh, step-up scopes, web remote reconnect) — see [Mid-session auth](v2_auth_mid_session.md)
|
||||
* **Authorization hardening** (MCP 2026-07-28 SEPs: `iss`, DCR client type, issuer-bound credentials, step-up scope union) — see [Auth hardening](v2_auth_hardening.md)
|
||||
* **OAuth runtime persistence** — `OAuthStorageBase` + file/remote backends; shared `oauth.json` across web/CLI/TUI (#1548, #1549)
|
||||
* **OAuth smoke testing** (manual procedures against hosted servers) — see [Smoke testing](v2_auth_smoke_testing.md)
|
||||
|
||||
## Transport Types
|
||||
* STDIO
|
||||
* SSE
|
||||
* SHTTP
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ Replaces the hardcoded `SEED_SERVERS` in `clients/web/src/App.tsx:47` with a fil
|
|||
## File location
|
||||
|
||||
- **Path**: `~/.mcp-inspector/mcp.json` (Windows: `%USERPROFILE%\.mcp-inspector\mcp.json`).
|
||||
- **Why this dir**: `~/.mcp-inspector/storage/` already exists for the Zustand-persist stores (OAuth, settings); one Inspector dir under `$HOME` is friendlier than two. Resolution uses the same `process.env.HOME || process.env.USERPROFILE` fallback as `getDefaultStorageDir()` in `core/storage/store-io.ts:13`.
|
||||
- **Why this dir**: `~/.mcp-inspector/storage/` already holds runtime persistence files (OAuth tokens, install `client.json`, etc.); one Inspector dir under `$HOME` is friendlier than two. Resolution uses the same `process.env.HOME || process.env.USERPROFILE` fallback as `getDefaultStorageDir()` in `core/storage/store-io.ts:13`.
|
||||
- **Why canonical filename**: lets users symlink to/from Claude Desktop and similar tools.
|
||||
- **Permissions**: `0o600`, matching `writeStoreFile` in `core/storage/store-io.ts:55`.
|
||||
|
||||
|
|
@ -88,9 +88,9 @@ If the file does not exist when the backend boots, write a file containing the t
|
|||
| Hono backend + auth + storage routes pattern | `core/mcp/remote/node/server.ts` | `/api/storage/:storeId` is the template for the new `/api/servers` routes |
|
||||
| Auth'd fetch from browser | wired via `getAuthToken()` in `clients/web/src/App.tsx:84` | `useServers` will call the backend with `x-mcp-remote-auth: Bearer <token>` |
|
||||
|
||||
### Why not `createFileStorageAdapter` directly
|
||||
### Why not a `{ state, version }` envelope for `mcp.json`
|
||||
|
||||
`core/storage/adapters/file-storage.ts` is a Zustand `persist` adapter — it wraps the payload as `{ state, version }` so the file ends up looking like `{"state":{...},"version":0}`. That breaks the "human-editable canonical `mcp.json`" goal. We use the underlying `store-io.ts` primitives instead.
|
||||
Older OAuth persistence used a middleware-style envelope `{ state, version }` around the payload. That shape is fine for opaque runtime blobs like `oauth.json` (still accepted on **read** for migration — see [OAuth persistence](v2_auth_ema.md#oauth-persistence-1549--done)) but breaks the goal of a **human-editable canonical `mcp.json`**. Server list I/O uses the underlying `store-io.ts` primitives and writes plain JSON.
|
||||
|
||||
### New code
|
||||
|
||||
|
|
|
|||
|
|
@ -22,13 +22,14 @@ This specification defines the storage and state management architecture for Ins
|
|||
|
||||
| Category | Examples | Persistence | Location |
|
||||
|----------|----------|-------------|----------|
|
||||
| **OAuth runtime state** | Tokens, PKCE, scopes, IdP sessions, AS metadata | File (`oauth.json`) | `OAuthStorageBase` + persist backends |
|
||||
| **Server Configurations** | URL, transport, headers | File (mcp.json) | Proxy server |
|
||||
| **User Preferences** | Theme, log level, panel sizes | localStorage | Browser |
|
||||
| **Connection State** | Status, server info, errors | Memory | React Context |
|
||||
| **Execution State** | Current request, pending queue | Memory | React Context |
|
||||
| **Logs Display** | Filtered view, pause state | Memory | Zustand store |
|
||||
| **Execution Form State** | Selected tool, form values | Memory | Zustand store |
|
||||
| **Testing Profiles** | Custom profiles, active selection | localStorage | Zustand store |
|
||||
| **Logs Display** | Filtered view, pause state | Memory | `core/mcp/state` + React hooks |
|
||||
| **Execution Form State** | Selected tool, form values | Memory | React component state / hooks |
|
||||
| **Testing Profiles** | Custom profiles, active selection | localStorage | Browser (planned) |
|
||||
| **History Data** | Request/response records | NDJSON file (Pino) | Proxy server |
|
||||
|
||||
### Server-Side vs Client-Side Storage
|
||||
|
|
@ -38,9 +39,9 @@ This spec focuses on **client-side** state management. Server-side persistence u
|
|||
| Layer | Technology | Purpose |
|
||||
|-------|------------|---------|
|
||||
| **Proxy (Pino)** | NDJSON files | Raw history persistence (`history.ndjson`), parsed by History API |
|
||||
| **Client (Zustand)** | localStorage + memory | UI state, display buffers, user preferences |
|
||||
| **Client (browser UI)** | localStorage + memory | UI preferences, display buffers |
|
||||
|
||||
The Pino logger on the proxy writes MCP request/response records to NDJSON format. The History API endpoint parses this file and returns filtered JSON. Client-side Zustand stores handle:
|
||||
The Pino logger on the proxy writes MCP request/response records to NDJSON format. The History API endpoint parses this file and returns filtered JSON. Client-side `core/mcp/state` stores and React hooks handle:
|
||||
- How logs are **displayed** (filters, pause, auto-scroll)
|
||||
- Caching fetched history for UI performance
|
||||
- User preferences that don't belong on the server
|
||||
|
|
@ -49,6 +50,22 @@ See [v2_server.md](./v2_server.md#pino-rationale) for Pino configuration details
|
|||
|
||||
---
|
||||
|
||||
## OAuth runtime persistence
|
||||
|
||||
OAuth and EMA **runtime state** (access/refresh tokens, PKCE verifiers, granted scopes, cached authorization-server metadata, IdP sessions) is stored separately from `mcp.json` in **`~/.mcp-inspector/storage/oauth.json`** by default.
|
||||
|
||||
| Client | Implementation | Path |
|
||||
| ------ | -------------- | ---- |
|
||||
| **Web** | `RemoteOAuthStorage` → `/api/storage/oauth` | Same on-disk file via Hono backend |
|
||||
| **CLI / TUI** | `NodeOAuthStorage` | Direct file I/O |
|
||||
| **Tests / reference** | `BrowserOAuthStorage` | sessionStorage (not wired in v2 web app) |
|
||||
|
||||
**Stack (#1549):** `OAuthStorage` interface → `OAuthStorageBase` + `OAuthMemoryStore` + `OAuthPersistBackend` (`core/auth/oauth-storage.ts`, `core/auth/store.ts`, `core/auth/oauth-persist.ts`). Writes plain JSON `{ servers, idpSessions }`. Reads still promote legacy `{ state, version }` envelopes (migrate-on-write). All getters are async; setters auto-persist. Web shares one store via `getWebRemoteOAuthStorage()`.
|
||||
|
||||
Details: [EMA auth](v2_auth_ema.md#oauth-persistence-1549--done), [Mid-session auth](v2_auth_mid_session.md).
|
||||
|
||||
---
|
||||
|
||||
## State Management Options Analysis
|
||||
|
||||
### Comparison Matrix
|
||||
|
|
@ -197,6 +214,8 @@ This pattern is already implemented in `McpContext.tsx` and working well. No mig
|
|||
|
||||
## Zustand Store Specifications
|
||||
|
||||
> **Historical note:** This section captured an early plan to use [Zustand](https://docs.pmnd.rs/zustand) for browser UI state. The **shipped** web client uses **`core/mcp/state`** event-driven stores and **`core/react`** hooks instead. **OAuth runtime persistence** uses **`OAuthStorageBase`** (see [OAuth runtime persistence](#oauth-runtime-persistence) above), not Zustand. The comparison matrix below remains useful for evaluating future UI-state libraries.
|
||||
|
||||
### 1. Preferences Store
|
||||
|
||||
**Purpose:** Persist user preferences across sessions
|
||||
|
|
|
|||
|
|
@ -3,13 +3,13 @@
|
|||
### [Brief](README.md) | [V1 Problems](v1_problems.md) | [V2 Scope](v2_scope.md) | [V2 Tech Stack](v2_web_client.md) | V2 UX | [V2 Auth](v2_auth.md) | [V2 New Spec Impact](v2_new_spec_impact.md)
|
||||
#### [Overview](v2_ux.md) | [Features](v2_ux_features.md) | [Handlers](v2_ux_handlers.md) | [Screenshots](v2_screenshots.md) | Components | [Interfaces](v2_ux_interfaces.md)
|
||||
|
||||
We are developing React components with Mantine, Zustand, and Storybook using a pattern called "Presentational Components" that emphasizes separation of concerns and testability.
|
||||
We are developing React components with Mantine, **`core/mcp/state` stores + `core/react` hooks**, and Storybook using a pattern called "Presentational Components" that emphasizes separation of concerns and testability.
|
||||
|
||||
## The Presentational / Container Split
|
||||
|
||||
The idea is that a component receives everything it needs through props — the data it renders *and* the callbacks it fires when the user interacts. The component itself has no idea where the data came from or what happens when a button is clicked. It just calls `onSave(formData)` and trusts that someone upstream is handling it.
|
||||
|
||||
This creates clean layering: Zustand stores own the state and logic, container components (or hooks) wire stores to presentational components, and the presentational components themselves are pure rendering machines. Storybook targets that bottom layer exclusively.
|
||||
This creates clean layering: **`core/mcp/state` stores** own InspectorClient-derived data; container components or hooks wire stores to presentational components; presentational components are pure rendering machines. Storybook targets that bottom layer exclusively.
|
||||
|
||||
## What Storybook Does
|
||||
|
||||
|
|
@ -47,11 +47,11 @@ This is where the real challenge lives, because React makes it *very* easy to le
|
|||
|
||||
**Props as the complete contract.** If a component needs to know whether a user has permission to delete something, don't pass in the user's role and let the component figure it out. Pass `canDelete: boolean`. The decision logic belongs in the store or a hook, not in the rendering layer. This also makes stories trivial to write — you just set `canDelete` to `true` or `false`.
|
||||
|
||||
**Callbacks over store access.** If a presentational component imports a Zustand store directly, it's no longer testable in isolation without mocking the store. Instead, have a thin wrapper hook or container that does `const { items, addItem } = useStore(...)` and passes those down. The presentational component just sees `items: Item[]` and `onAdd: (item: Item) => void`.
|
||||
**Callbacks over store access.** If a presentational component imports a global store directly, it's no longer testable in isolation without mocking. Instead, use a thin wrapper hook or container that reads from `useXxx()` hooks and passes props down. The presentational component just sees `items: Item[]` and `onAdd: (item: Item) => void`.
|
||||
|
||||
**Derived state lives outside.** Filtering, sorting, computed totals — all of that should happen in Zustand selectors or custom hooks, not inside the component. The component receives the *already filtered* list.
|
||||
**Derived state lives outside.** Filtering, sorting, computed totals — all of that should happen in store selectors or custom hooks, not inside the component. The component receives the *already filtered* list.
|
||||
|
||||
**Local UI state is the exception.** Things like "is this dropdown open" or "which tab is active" are legitimately component-internal. That's fine — that's UI state, not application state. The heuristic is: if the application would need to know about it (say, to restore state on navigation), it belongs in Zustand. If only the component cares, `useState` is appropriate.
|
||||
**Local UI state is the exception.** Things like "is this dropdown open" or "which tab is active" are legitimately component-internal. That's fine — that's UI state, not application state. The heuristic is: if the application would need to know about it (say, to restore state on navigation), it belongs in shared state (stores/hooks). If only the component cares, `useState` is appropriate.
|
||||
|
||||
## Mantine-Specific Considerations
|
||||
|
||||
|
|
@ -63,4 +63,4 @@ It's the most mature choice, but it's not the only one. Ladle is a lighter alter
|
|||
|
||||
For your stack, Storybook is the most well-trodden path and has the best TypeScript support for auto-generating controls from your prop types, which is genuinely useful. It will infer knobs/controls from your interfaces so you can tweak props interactively in the browser.
|
||||
|
||||
The bottom line: your instinct is right. Design your components around a typed model (the props interface), keep decision-making in Zustand, and Storybook becomes the natural place to develop and verify the visual layer independently. The discipline isn't really about Storybook — it's about prop interface design. Storybook just makes it obvious when you've violated the boundary, because a component that reaches outside its props becomes painful to write stories for.
|
||||
The bottom line: your instinct is right. Design your components around a typed model (the props interface), keep decision-making in hooks/stores, and Storybook becomes the natural place to develop and verify the visual layer independently. The discipline isn't really about Storybook — it's about prop interface design. Storybook just makes it obvious when you've violated the boundary, because a component that reaches outside its props becomes painful to write stories for.
|
||||
|
|
|
|||
Loading…
Reference in a new issue