Commit graph

2,270 commits

Author SHA1 Message Date
cliffhall
7f58b67ea6 refactor(web): rename Protocol list header "Requests" to "Messages" (#1623)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BrihGWcrM9JGRyu41nzZYw
2026-07-09 19:21:56 -04:00
cliffhall
ace4250d31 refactor(web): rename "History" tab/feature to "Protocol" (#1623)
Rename the web client's History tab and its supporting code to Protocol
so the feature reads as the MCP "Protocol" view.

- Tab renders as "Protocol"; INSPECTOR_TAB_IDS, InspectorView tab
  constants/MonitorTab, TAB_UI_REGISTRY key, and navigation.ts tab id all
  updated to Protocol.
- Rename components + folders: HistoryScreen -> ProtocolScreen,
  HistoryListPanel -> ProtocolListPanel, HistoryEntry -> ProtocolEntry,
  HistoryControls -> ProtocolControls, historyUtils -> protocolUtils
  (plus their tests and stories, and exported types).
- Rename App/InspectorView plumbing: historyUi, pinnedHistoryIds,
  on*History* handlers/props, replayHistoryRequest,
  isReplayableHistoryMethod, sort/compact scopes, etc. -> Protocol names.
- Export filenames use the "protocol" slug (buildExportFilename +
  ExportKind).

Preserved (per issue #1623 decisions):
- The in-panel section discriminator keeps its "pinned" | "history"
  values, and the sub-sections stay labeled Pinned / History.
- window.history and core message-log/replay concepts are untouched.

npm run ci passes (format, lint, build, unit tests, >=90 per-file
coverage gate, smoke, Storybook).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BrihGWcrM9JGRyu41nzZYw
2026-07-09 19:04:54 -04:00
Cliff Hall
0fde2d0024
Merge pull request #1622 from modelcontextprotocol/1621-open-monitoring-on-failure
feat(web): open the monitoring column on connection failure, with a Console tab and failed-card border (#1621)
2026-07-09 16:41:08 -04:00
cliffhall
a759777ceb refactor(web): content-gate History in the failure column (#1621)
Re-review follow-up (#1622):

- Observation A: a returning user whose last-pinned monitor tab was
  History would land on the (empty) History tab on a stdio failure instead
  of the Console diagnostic. Root cause: History is *always* empty on a
  connect failure (the message log clears on the error transition), yet it
  was pushed unconditionally, so it could precede/hide the real diagnostic.
  Fix: content-gate History like Console/Network — only offer it when it
  has entries. On a fresh failure the diagnostic (Console/Network) is the
  only tab, so it always leads, without touching tab-switching or adding
  state. This also resolves the earlier "opens onto an empty column" note:
  when nothing has been captured yet, monitorAvailable is empty and the
  column stays closed, opening reactively once stderr/fetch content lands.

Tests: stdio/HTTP failure now assert the diagnostic leads and History is
absent; added "leads with Console even when stored tab was History",
"keeps the column closed until a diagnostic has content", and "offers
History when it has captured content". Verified live (time server →
Console-only column). Full `npm run ci` passes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BrihGWcrM9JGRyu41nzZYw
2026-07-09 16:22:09 -04:00
cliffhall
87630f2356 refactor(web): address review — scope failure UX to connect attempts, dedupe timing (#1621)
Review feedback on #1622:

- #1 (medium): `connectionStatus === "error"` also covers a mid-session
  crash of a previously-connected server, not just a failed connect
  attempt. Scope the auto-open + failure tab set to connect attempts only:
  the auto-open effect's error arm now requires the previous status to not
  be `connected` (connecting/disconnected → error), and `failed` (render)
  requires `erroredServerId`, which the parent sets only for connect-
  attempt failures. A mid-session crash now closes the column like any
  teardown instead of reopening a user-closed column / dropping the live
  Logs tab. This also aligns the column behavior with the red border.
- #3 (minor): extract the column slide duration into a shared
  `monitorColumnAnimation.ts` (MONITOR_COLUMN_ANIM_MS +
  FAILED_CARD_SCROLL_DELAY_MS) so InspectorView's Transition and
  ServerCard's scroll delay can't drift.
- #4 (nit): composite key for ConsoleScreen entry rows instead of the
  array index.

Added a mid-session-crash test (connected → error with no erroredServerId
closes the column). Verified live: a connect failure still opens the
column to Console. Full `npm run ci` passes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BrihGWcrM9JGRyu41nzZYw
2026-07-09 16:06:33 -04:00
cliffhall
71d33d70c4 feat(web): scroll the failed server card into view on connection failure (#1621)
Opening the monitoring column on a failed connection reflows the server
grid to fewer/narrower columns, which can push the failed card below the
fold. Scroll it into view so the red-bordered card is visible without
hunting for it.

- ServerCard scrolls itself into view on the disconnected→errored
  transition (mirrors the freshly-added highlight scroll), deferred
  ~320ms so the column open + grid reflow settle first — otherwise
  scrollIntoView targets the card's pre-reflow position. Guarded by a ref
  so it fires once per failure and won't fight a user who scrolls away.

Verified live: the failed `time` card scrolls fully into view after the
column opens. Full `npm run ci` passes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BrihGWcrM9JGRyu41nzZYw
2026-07-09 15:05:34 -04:00
cliffhall
c6999dff96 feat(web): red border on a server card whose connection attempt failed (#1621)
Flags the server whose last connection attempt failed with a red card
border, cleared when another server is connected or a new connection is
attempted — a persistent, at-a-glance companion to the failure toast and
the auto-opened monitoring column.

- App tracks `failedServerId`: set in onToggleConnection's non-auth
  handshake catch, cleared at the start of any new connect attempt. It is
  deliberately kept out of resetSessionScopedUiState, since the failure's
  own `disconnect` event would otherwise clear it the same tick (that same
  event clears `activeServer`, which is why the flag is tracked separately).
- New `errored` Card theme variant (red border via a new
  `--inspector-error-border` token), mirroring the `highlighted` green
  border. Precedence: disabled (dimmed) > errored > highlighted.
- Plumbed `erroredServerId` through InspectorView → ServerListScreen →
  ServerCard (`errored` prop).

Full `npm run ci` passes (validate + coverage + smoke + storybook).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BrihGWcrM9JGRyu41nzZYw
2026-07-09 14:48:17 -04:00
cliffhall
46b311ad5c test(web): pass Console props in InspectorView stories (#1621)
The InspectorView story args predate the new required Console props, so the
"Connection Error" play story crashed rendering (`stderrLogs` undefined →
`.length` throw) — caught by the Storybook step of `npm run ci`, which I
should have run before the previous push.

- Add `stderrLogs` / `consoleUi` / `onConsoleUiChange` / `onClearConsole` /
  `onExportConsole` to the story args, with a `demoStderr` fixture.
- Make the Connection Error story a stdio-failure narrative: it now opens
  the column to Console showing the captured startup error, with `network:
  []` so Network isn't offered.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BrihGWcrM9JGRyu41nzZYw
2026-07-09 14:25:46 -04:00
cliffhall
219298b24d feat(web): add Console (stderr) tab; show it on stdio connect failure (#1621)
A failed stdio launch had no useful diagnostic surface: the message log
(History) is cleared on the error transition and there are no HTTP
requests (Network), so the auto-opened monitoring column was empty. The
process's stderr — which explains the failure (e.g. "No module named
mcp_server_time") — was captured in StderrLogState but never displayed.

- New ConsoleScreen renders the captured stderr stream (search / sort /
  clear / export; embeddable in the monitoring column), wired through App
  via the existing useStderrLog hook + StderrLogState.
- Console is a pinnable monitor tab like Logs/History/Network, gated to
  stdio servers (HTTP has no child process). It lives in the header menu
  when the column isn't pinned, and moves into the column when it is.
- The failure column now picks its tabs by which diagnostic actually
  captured content (stderr → Console, fetch requests → Network), not by
  the active server's transport: a connect failure fires the client
  `disconnect` event, which clears `activeServer`, so transport can't be
  read back there. The memo re-runs as stderr/fetch entries stream in, so
  the diagnostic tab appears as soon as the failing process prints.

Verified live against a failing `python -m mcp_server_time` stdio server:
the column auto-opens to Console showing the real ModuleNotFoundError.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BrihGWcrM9JGRyu41nzZYw
2026-07-09 14:06:13 -04:00
cliffhall
85153c6232 feat(web): open the monitoring column on a failed connection (#1621)
On a connection failure the user was left without an obvious way to see
why. Open the monitoring column automatically on the transition into the
`error` status (mirroring the connect-success behavior from #1616) so the
diagnostics surface right when they're needed.

- Broaden the auto-open effect to fire on the transition into `error` as
  well as `connected`, tracked via a general previous-status ref so it
  fires once per transition and never on a mount that starts errored.
- A failed connect never negotiated capabilities, so the failure column
  offers History (the local client-side message log) and — for non-stdio
  transports — Network (HTTP requests), the entries that explain the
  failure. Logs is omitted (it needs the server `logging` capability).
- `effectivePinned` now allows the column while `error`, not only while
  `connected`; it still closes on a clean disconnect or narrowing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BrihGWcrM9JGRyu41nzZYw
2026-07-09 13:12:32 -04:00
Cliff Hall
89e9a275ef
Merge pull request #1620 from modelcontextprotocol/1619-ci-coverage-parity
fix: make `npm run ci` a true superset of GitHub CI (coverage gate)
2026-07-09 13:02:45 -04:00
cliffhall
812bdb0cc0 chore: remove orphaned ci:integration script
The root ci:integration alias became dead after ci switched to run
coverage (which runs web's unit+integration projects under v8). Its
only occurrence was its own definition. clients/web test:integration
remains available for direct manual runs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BrihGWcrM9JGRyu41nzZYw
2026-07-09 12:35:02 -04:00
cliffhall
0326e32eef Merge branch 'v2/main' into 1619-ci-coverage-parity
# Conflicts:
#	AGENTS.md
2026-07-09 11:22:48 -04:00
Cliff Hall
822ac251e7
Merge pull request #1601 from modelcontextprotocol/1579-wave-1
Wave 1 rollup: integrated 1579-wave-1 (#1600)
2026-07-09 11:17:21 -04:00
cliffhall
edc83c49a8 fix: make npm run ci a true superset of GitHub CI (coverage gate)
The local `npm run ci` was not at parity with `.github/workflows/main.yml`.
CI runs `validate → coverage → smoke → test:storybook`, but the root `ci`
script ran `validate → ci:integration → smoke → ci:storybook`, omitting
`npm run coverage` — the per-file ≥90 (lines/statements/functions/branches)
gate that CI enforces. So a push could pass every documented local pre-push
command and still fail CI on the coverage gate (this exact gap broke PR #1601
on a HistoryEntry.tsx function-coverage regression).

Two changes:

1. Root `package.json` `ci` script now chains `npm run coverage` in place of
   `ci:integration`. Because web's `coverage:web` (`test:coverage`) already
   runs the integration project under instrumentation, `coverage` supersedes
   the old integration-only step and makes `ci` a genuine superset of GitHub
   CI. The `ci:integration` / `ci:storybook` script definitions are left in
   place for any other references.

2. AGENTS.md: rename the pre-push section to "Mandatory pre-push gate", make
   running the coverage gate a hard non-optional step (remove the "or at
   minimum `npm run validate`" loophole and the soft "optionally also run
   coverage" phrasing), and correct the parity claim so `npm run ci` is
   described as `validate → coverage → smoke → storybook`.

Closes #1619

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BrihGWcrM9JGRyu41nzZYw
2026-07-09 10:45:52 -04:00
cliffhall
97deacd875 test(web): cover compact-layout expand toggle in HistoryEntry
Adds a test that clicks Expand/Collapse in the embedded (compact)
layout, exercising the ExpandToggle onToggle callback that was
uncovered (line 193). Restores HistoryEntry.tsx to 100% coverage,
fixing the CI per-file function-coverage gate failure (was 85.71%).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BrihGWcrM9JGRyu41nzZYw
2026-07-09 10:19:59 -04:00
cliffhall
32a3610737 Merge remote-tracking branch 'origin/v2/main' into 1579-wave-1
# Conflicts:
#	clients/cli/package-lock.json
#	clients/cli/package.json
#	clients/web/src/App.tsx
#	clients/web/src/lib/environmentFactory.test.ts
#	clients/web/src/lib/environmentFactory.ts
#	clients/web/src/lib/remoteOAuthStorage.test.ts
#	clients/web/src/lib/remoteOAuthStorage.ts
#	clients/web/src/test/core/auth/connection-state.test.ts
#	clients/web/src/test/core/auth/providers.test.ts
#	clients/web/src/test/core/auth/storage-browser.test.ts
#	clients/web/src/test/core/auth/storage-remote.test.ts
#	clients/web/src/test/core/mcp/oauthManager.test.ts
#	clients/web/src/test/integration/auth/node/storage.test.ts
#	clients/web/src/test/integration/storage/adapters.test.ts
#	clients/web/src/utils/clearServerOAuthState.test.ts
#	clients/web/src/utils/clearServerOAuthState.ts
#	core/auth/connection-state.ts
#	core/auth/oauth-storage.ts
#	core/auth/providers.ts
#	core/auth/storage.ts
#	core/auth/store.ts
#	core/storage/adapters/remote-storage.ts
2026-07-09 09:41:08 -04:00
Cliff Hall
bd74c356ec
Merge pull request #1617 from modelcontextprotocol/v2/1616-pin-monitoring-column
feat(web): pin Logging/History/Network as a resizable column (#1616)
2026-07-08 22:35:22 -04:00
cliffhall
20e08e2b1d feat(web): open the monitoring column on connect (#1616)
Auto-open the monitoring column when a connection is established. Gated on the
disconnected → connected transition (a ref-tracked edge), so it fires on an
actual connect rather than on every render while connected or on a mount that
starts already-connected (which would fight a user who closed it). The column
still only appears when wide and a monitor tab is available.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 22:26:06 -04:00
cliffhall
efddd94bea fix(web): use a time-only timestamp in the compact column entries (#1616)
At the 320px column minimum the full ISO timestamp ate most of line 1. Use an
HH:MM:SS (UTC) form in the embedded HistoryEntry/NetworkEntry headers only; the
full-size layouts keep the full ISO string.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 20:59:23 -04:00
cliffhall
3550b64032 feat(web): compact two-line HistoryEntry/NetworkEntry for the narrow column (#1616)
In the pinned monitoring column (embedded), the single-line entry headers wrap
badly. Add compact two-line variants gated on `embedded`:

- HistoryEntry: line 1 = time + client/server badge + elapsed + status; line 2 =
  method (and target, truncated) on the left with the controls on the right,
  where Replay becomes an icon (new ReplayButton element) next to Pin/Expand.
- NetworkEntry: line 1 = time + HTTP method + category + elapsed + status; line 2
  = the URL in a horizontal ScrollArea (new Text `nowrap` variant) with the
  expand toggle on the right.

The full-size (non-embedded) layouts are unchanged. `embedded` is threaded from
the History/Network panels into the entries.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 20:53:23 -04:00
cliffhall
bcf10c75dd style(web): put the timestamp first in HistoryEntry to match NetworkEntry
Move the Client/Server direction badge to the right of the timestamp so the
monospace timestamp is leftmost, matching the NetworkEntry header layout.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 20:28:48 -04:00
cliffhall
aba77d47f5 fix(web): restore resize handle + refine column close/animation behavior (#1616)
- Fix the resize handle disappearing: the animation wrapper is a Mantine Group
  (default align: center), and the full-height handle has no intrinsic height, so
  it collapsed to 0. Set align: stretch on the wrapper.
- Closing the column now leaves the primary on the screen currently selected in
  the header (commits the active tab first), instead of swapping in the column's
  screen.
- Keep the primary screen's standard ScreenStage transition; only the column
  (handle + column) slides open/closed from the right with a fade.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 20:25:36 -04:00
cliffhall
c5e064ff71 feat(web): monitoring column search, close-to-fullscreen, and open/close animation (#1616)
- Add a search box to MonitoringControls (right of the tabs). It's a single
  shared search kept across column tab switches, filtering whichever screen is
  shown. Embedded panels apply this text but still ignore the sidebar-only
  filters (levels/categories/directions/method), which have no control there.
- Close button now uses the "collapse sidebar" icon (TbLayoutSidebarRightCollapse),
  the mirror of the pin button's expand icon.
- Closing the column carries its selection to the primary view: the screen the
  user was watching becomes the active header tab (rather than snapping back to
  the pre-pin selection).
- Animate the column open/close: the handle + column slide in from the right and
  fade (reversing on close) via a Mantine Transition.
- HistoryListPanel: drop the redundant "History (N)" header when there are no
  pinned requests (lone unpinned section renders headerless).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 20:16:15 -04:00
cliffhall
9b76272ad1 fix(web): address review feedback on the monitoring column (#1616)
- ResizeHandle: clear the `resizing-col` body class on unmount so a drag
  interrupted by the handle unmounting (narrow past the breakpoint / disconnect)
  can't leave page-wide user-select:none / col-resize applied.
- Embedded monitor column now shows the full unfiltered stream instead of
  silently mirroring the full-size screen's live filter (the column has no
  filter control to explain an empty/partial list).
- MonitoringControls: give the SegmentedControl an aria-label ("Monitoring
  screen") so the tab group has an accessible name.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 19:41:00 -04:00
cliffhall
daf736c6e7 style(web): match the pin-as-column button to the expand/collapse toggle
Render PinColumnButton as a subtle size-sm Button with a size-20 icon, the same
style as the panel's ListToggle, instead of a default ActionIcon.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 19:26:35 -04:00
cliffhall
591d6b9736 refactor(web): move expand/collapse toggle between Export and the pin button
On the History and Network panels, place the ListToggle (expand/collapse)
after Export and before the pin-as-column button. (Logs has no such toggle.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 19:24:25 -04:00
cliffhall
3713e71b18 refactor(web): move the pin-as-column button to the far right of each toolbar
Place the "pin as column" icon after Clear/Export (rightmost) on the Logs,
History, and Network panel toolbars instead of at the front.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 19:20:24 -04:00
cliffhall
081fd789f1 feat(web): pin Logging/History/Network as a resizable column (#1616)
Add the ability to "pin" the Logging, History, and Network screens into a
second column on the right of the InspectorView, so a primary screen and a
monitor screen are visible at once (e.g. watch Logs while calling a Tool).

- A "pin as column" button on the Logs/History/Network panel toolbars opens
  the column (shown only when wide + connected).
- The column hosts a MonitoringControls tab row (available monitor tabs +
  close) over the selected embedded screen. Pinning removes the monitor group
  from the header tab bar; closing (x) returns it.
- The divider between the primary area and the column is draggable (pointer +
  keyboard); width is clamped and persisted.
- The split collapses to one column below ~1040px (the ServerListScreen
  single-card breakpoint) and while disconnected, without discarding the pin
  preference — it reopens when wide + connected again.

State (pinned/tab/width) is view-local and persisted via the existing
useLocalStorage pattern, so no new props thread through App and ViewHeader is
unchanged (it just receives a filtered tab list). Screens/panels gain an
`embedded` mode (fills the parent height, drops the 340px filter sidebar) via a
shared EmbeddableScrollArea element.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 18:52:41 -04:00
Cliff Hall
4ee9bbce0f
Merge pull request #1612 from modelcontextprotocol/v2/1549-oauth-storage-no-zustand
refactor(auth): remove Zustand from OAuth persistence, clean up storage IO (#1549)
2026-07-08 17:11:16 -04:00
cliffhall
98517c2758 test(auth): fix type error in remote backend write() test
`tsc -b` (run in CI's validate step, but not by vitest locally) rejected
`ok.mock.calls[0][1].body` — under the project's strict indexing the
untyped mock's call tuple is `[]`, so `[0]` is possibly-undefined and
`[1]` is out of range. Capture the request body inside a `vi.fn<typeof
fetch>` implementation instead of indexing into `mock.calls`.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 16:43:41 -04:00
cliffhall
4c30dc6e2e test(auth): make the browser-safe guard transitive
Extend the Node-free guard beyond oauth-persist.ts to every module
reachable from the browser's RemoteOAuthStorage (storage-remote,
oauth-storage, store, oauth-persist, store-serialize), so a forbidden
store-io/node:/atomically import added one hop away is caught too — not
just a direct import in oauth-persist.ts. Addresses the non-blocking
hardening note on Claude review of #1612.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 16:25:14 -04:00
cliffhall
1e6a2825d4 fix(auth): keep oauth-persist browser-safe (move Node file backend out)
The refactor routed the browser's RemoteOAuthStorage through
`oauth-persist.ts`, which value-imported the Node-only `store-io.js`
(`atomically` -> `stubborn-fs` -> `node:process.getuid`, plus
`node:fs/promises`). That dragged Node built-ins into the browser bundle
and the web app crashed to a blank page at runtime — a regression from
v2/main, where browser OAuth storage used a Zustand JSON adapter with no
Node imports. Unit/integration tests run in node/happy-dom and
`smoke:web` only checks the served HTML, so none of them execute the app
in a real browser and the break slipped through.

Fix, following the existing `store-id.ts` precedent:
- Split the pure JSON (de)serializers into Node-free
  `core/storage/store-serialize.ts`; `store-io.ts` re-exports them so
  existing importers are unaffected.
- Move the Node-only `createFileOAuthPersistBackend` into
  `core/auth/node/oauth-persist-file.ts`; update `storage-node.ts` and
  the storage adapters test to import from there.
- `oauth-persist.ts` now imports only the Node-free serializers, so the
  browser bundle no longer pulls in `node:fs`/`atomically`.

Tests:
- oauth-persist-browser-safe.test.ts: source-level guard that
  `oauth-persist.ts` never imports store-io/node:/atomically.
- Direct unit tests for the isomorphic remote + session backends
  (restoring oauth-persist.ts coverage after the file backend moved out).
- store.test.ts: OAuthMemoryStore unit coverage incl. replace() fallbacks.
- storage.test.ts: clearAllOAuthClientState null-snapshot (no file) path.

Verified end-to-end in a real browser (Playwright): DCR OAuth flow
completes, tokens persist via the remote store, and after a full page
reload the server reconnects with no re-authentication.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 16:20:31 -04:00
cliffhall
cdeb2316d0 test(auth): assert parseOAuthPersistBlob({}) returns null
Document the contract the remote read() path leans on — the server's
missing-file response is `c.json({}, 200)`, and the backend now passes
that empty object straight to parseOAuthPersistBlob. Addresses the
optional note on Claude review of #1612.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 13:57:16 -04:00
cliffhall
54d5b18b1e fix(auth): drop redundant empty-object guard in remote OAuth read()
parseOAuthPersistBlob already returns null for {} (the server's
missing-file response) and for a literal null body, so the
Object.keys(store).length === 0 guard was redundant — and would throw a
TypeError on a null body before parseOAuthPersistBlob could handle it.
Pass the parsed res.json() straight through.

Addresses Claude review item #1 on #1612.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 13:54:14 -04:00
cliffhall
edc7dbb1c1 Merge remote-tracking branch 'origin/v2/main' into v2/1549-oauth-storage-no-zustand
# Conflicts:
#	core/auth/store.ts
2026-07-08 13:44:12 -04:00
Cliff Hall
87133bd42d
Merge pull request #1609 from modelcontextprotocol/v2/1548-web-remote-oauth-storage
refactor: migrate to RemoteOAuthStorage for web OAuth handling
2026-07-08 13:38:34 -04:00
cliffhall
0b969e2d43 Merge remote-tracking branch 'origin/v2/main' into review-1609 2026-07-08 11:14:45 -04:00
cliffhall
ba48e06ffd fix(auth): guard OAuth-storage async paths against unhandled rejections
Review follow-ups on the RemoteOAuthStorage web migration:

- useEmaIdpLoginState.logout(): add a .catch to the now-async
  clearEmaIdpSession call so a storage failure no longer surfaces as an
  unhandled rejection; leave loginState unchanged on failure (the session
  was not actually cleared). Add a test asserting no unhandled rejection.
- createOAuthStore: attach a no-op catch to the eager persistLoadPromise
  so a hydration read that fails before any load() awaits it does not
  become an unhandled rejection; real awaiters still observe the rejection
  via the shared reference.
- storage-remote.test: replace `as unknown as typeof fetch` double-casts
  with the typed vi.fn<typeof fetch>() pattern the same PR established.
- remoteOAuthStorage: correct the doc comment to describe the cache-of-one
  accurately instead of implying a keyed map.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 10:59:40 -04:00
Cliff Hall
bdd2edc15e
Merge pull request #1611 from modelcontextprotocol/1610-midsession-coverage
test: cover v2/main mid-session-auth code to the ≥90% per-file gate
2026-07-06 10:35:25 -04:00
Bob Dickinson
af174666d6 fix(auth): address Claude review on OAuth persist serialization
Serialize persist() in OAuthStorageBase so overlapping remote POSTs cannot
reorder and clobber state. Regenerate launcher package-lock.json to drop
stale zustand entries. Let parseOAuthPersistBlob accept parsed objects
from res.json() without a redundant JSON round-trip.
2026-07-05 18:38:13 -07:00
Bob Dickinson
3c65bdb4c6 Removed Zustand storage, cleanup up auth storage IO interfaces, implementations, and usage. 2026-07-04 21:56:45 -07:00
cliffhall
1f7ef9184c test(tui): truly assert the non-selected-server auth guards
Addresses the @claude review of #1611: the event registry is now keyed by
(client, fn) rather than event name alone, and a new `fireClientEventFor(client,
…)` helper fires a single client's listeners. The "ignores auth lifecycle events
from a non-selected server" test now fires ambient/recovered/oauthError for ONLY
the non-selected client and asserts no message appears — so removing the
`selectedServerRef.current !== serverName` guard would fail the test — then fires
for the selected client to confirm it does act. Coverage of App.tsx is unchanged
(92.23 / 90.20 / 92.85 / 92.68); existing fire-all `fireClientEvent` behavior is
preserved.

Part of #1610.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S3fTN8H3R8YV4yUGvZjYnX
2026-07-04 19:08:44 -04:00
cliffhall
f7ab287f6a test(tui): cover App.tsx mid-session auth flows to the ≥90% gate
Bring clients/tui/src/App.tsx from 79% to ≥90 on all four dimensions
(92.23 / 90.20 / 92.85 / 92.68) by exercising the mid-session / step-up auth
machinery that the existing suite didn't reach:

- runOAuthAuthentication result branches (success / already_authorized /
  insufficient_scope / unsupported), driven deterministically via a new opt-in
  runRunnerInteractiveOAuth override (defaults to the real runner so the
  existing callback-driven OAuth tests are unchanged).
- The per-client auth-lifecycle listeners (authChallengeAmbient / Recovered /
  Interactive / oauthError), fired through a new client-event registry on the
  FakeClient, including the non-selected-server early-return guards.
- handleAuthRecoveryRequired step-up-confirm and reauth paths, handleConnect's
  OAuth-on-401 branches (AuthRecoveryRequired + EmaClientNotConfigured thrown
  mid-flow), handleClearOAuth's connected/disconnect path, and the AuthTab
  onAuthorizeStepUp handler (EMA satisfied/interactive/failed/throw + standard
  non-EMA authorize, cancel).

Part of #1610.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S3fTN8H3R8YV4yUGvZjYnX
2026-07-04 17:24:10 -04:00
cliffhall
9a3604d01d test(tui): cover AuthTab step-up up-arrow and a/c shortcuts
Bring clients/tui/src/components/AuthTab.tsx to ≥90 on all four dimensions
(now 97.43/90.81/100/100) by exercising the previously-untested step-up
keypress paths in the `useInput` handler: up-arrow navigation back to the
"authorize" choice, the `a` (authorize) and `c` (cancel) shortcuts, and the
fall-through for an unrelated key while the prompt is pending.

Part of #1610.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S3fTN8H3R8YV4yUGvZjYnX
2026-07-04 16:59:51 -04:00
cliffhall
01cc79c61b test(cli): cover cliOAuth.ts step-up/recovery paths to the ≥90% gate
Bring clients/cli/src/cliOAuth.ts from 75% to ≥90% on all four coverage
dimensions (now 100/96.42/100/100) by exercising the previously-untested
interactive step-up and auth-recovery paths:

- confirmStepUpFromStdin (the default stdin confirmer): drive it via the
  default `confirmStepUp` parameter of handleCliAuthRecoveryRequired with
  node:readline/promises mocked — covers the y / "yes" (trimmed,
  case-insensitive) accept paths and the n decline path (which throws).
- connectInspectorWithOAuth's AuthRecoveryRequiredError branch: both the
  storage-already-satisfies resume and the interactive-recovery sub-branches,
  plus the isUnauthorizedError branch (with a rejecting disconnect to hit the
  `.catch(() => {})` guard), the non-OAuth-capable-config rethrow, and the
  non-OAuth-error rethrow.

Part of #1610 — v2/main mid-session-auth code below the per-file coverage
gate that v2/main's CI does not run but the Wave-1 rollup (#1550) does.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S3fTN8H3R8YV4yUGvZjYnX
2026-07-04 16:47:48 -04:00
cliffhall
c026d4d84a docs: address review nits on the v2/main merge
- transport.ts: update the SSE-branch comment to say the default path uses
  fetchWithOptionalAuthIntercept (proxy-aware baseFetch + optional auth-challenge
  intercept), not the bare proxy-aware baseFetch.
- clients/cli/README.md: fix the §5 mid-session validation link to use the
  ../../specification/ prefix like the sibling links in the same sentence
  (previously resolved to a nonexistent clients/cli/ path).

Both flagged by the @claude review of PR #1601; comment/doc-only, no runtime change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S3fTN8H3R8YV4yUGvZjYnX
2026-07-04 15:42:22 -04:00
Bob Dickinson
512f45d357 fix(auth): reject load() when persist read fails 2026-07-03 14:50:04 -07:00
cliffhall
cfbdfdf2bd Merge remote-tracking branch 'origin/v2/main' into 1579-wave-1
# Conflicts:
#	AGENTS.md
#	clients/cli/README.md
#	clients/web/src/App.test.tsx
#	clients/web/src/App.tsx
#	clients/web/src/test/core/auth/utils.test.ts
#	core/mcp/node/transport.ts
#	test-servers/src/composable-test-server.ts
2026-07-03 17:39:07 -04:00
Bob Dickinson
a24091484c fix(auth): await OAuth storage load() to fix remote OAuth races 2026-07-03 14:18:00 -07:00