* web: accessible light/dark color palette + dark-mode legibility fixes (#1678)
Ports the design system's accessible palette. Colors are defined once as Mantine
color tuples in theme.ts (shade 0 lightest → 9 darkest); every `--inspector-*`
token in App.css references `--mantine-color-*`, so improving the tuples cascades
through the whole UI.
- theme.ts: full accessible tuple set (inspector/gray/dark/green/red/yellow/blue/
teal/violet), WCAG-AA at the primary shades. Tuples are the one place literal
hex belongs — the token source. Primary stays inspector-7 (light) / -8 (dark).
- theme/ActionIcon.ts: default `color: "gray"` so chrome icons (settings, theme
toggle, sidebar, pin, sort, replay) render neutral grey instead of inheriting
the blue primary. Icons that set their own color/variant still override.
- App.css: fix light surfaces (`surface-body` gray-0, `surface-card` white — the
body token pointed at near-black dark-9); add direction/method badge tokens;
lift the semantic status/log/task/history colors 1–2 steps in the dark scheme
(the shared `*-6` shade sat too dark on charcoal). Light `:root` keeps `*-6`.
- Badges: MessageDirectionBadge + new MethodBadge element driven by
`--inspector-badge-*` tokens (deep fill + light text in dark, tinted in light)
via Mantine `bg`/`c` style props with `autoContrast={false}`; swaps
`<Badge color="dark">{method}</Badge>` in ProtocolEntry/NetworkEntry (their
Badge import stays — both still use it for status/category chips). Stories +
test added for MethodBadge (≥90 coverage gate).
Domain badge semantics live in the element components, not theme files. No raw
hex outside the tuples; no inline styles. Supersedes the reverted #1677.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BrihGWcrM9JGRyu41nzZYw
* web: WCAG AA accessibility pass — contrast, labels, names, roles (#1678)
Drives the axe-core ruleset (the engine behind Storybook's a11y addon) to
zero violations across all stories in light and dark mode.
Contrast (805 → 0 hits):
- Darken Mantine `*-light-color` + `dimmed` in light mode (and lift dark
`dimmed`) via `!important` App.css overrides — Mantine injects these at
runtime at matching specificity, so specificity alone loses the tie.
- `filledBadgeColor` pins amber (orange/yellow) filled badges to shade 5 so
`autoContrast` (JS-computed from the theme shade, not CSS vars) picks black.
- `accessibleTextColor` routes colored log/filter text through the scheme-aware
`-light-color` token so it clears AA on light and dark surfaces.
- `status` Badge variant: amber statuses use black-on-amber (orange text can't
reach AA on light at any Mantine shade).
- Resource URIs use `-light-color` (blue-4 was 4.38:1 in dark).
Names / roles:
- `aria-dialog-name`: custom-header modals use `Modal.Title` (wires the
dialog's `aria-labelledby`) instead of a bare `Title` + a root `aria-label`
(which also tripped `aria-prohibited-attr`).
- `button-name`: a `Modal` theme default labels every built-in close button.
- `label`: `aria-label` on the previously-unlabelled Textarea/Select inputs.
- `scrollable-region-focusable`: keyboard-focusable content Code/ScrollArea
viewports; long OAuth values wrap instead of scrolling.
- `heading-order`: correct heading levels (keeping visual size via `size`) so
outlines don't skip a level under the h2 modal/section titles.
Scrollbars: `type="scroll"` ScrollArea theme default — scrollbars auto-hide and
show only while scrolling (a deliberate product choice over the always-visible
a11y default).
`region` is disabled in the Storybook a11y config: isolated component/screen
stories render outside the app's `AppShell` landmarks, so it's a page-level
concern the components can't own.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* web: let the resource template form fill the main-area width (#1678)
Drop the maw=40% cap on the template-form pane so the URI preview and
variable inputs use the full width beside the sidebar, matching the
resource preview pane.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* web: left-align + reverse the input-panel action rows (#1678)
Move the footer action buttons to the left (justify flex-start) and put
the primary action first on the Tool / Prompt / Resource-preview /
Resource-template panels — Subscribe, Refresh, View Source on the
preview panel; Execute before Cancel on the tool panel. Shortens the
pointer travel from the sidebar controls / the form fields above to the
actions, which matters most when the monitoring sidebar is hidden and
the form is wide.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* web: match the dark-mode body background to the header tab bar (#1678)
Set the dark `--inspector-surface-body` to `dark-8` (the Mantine
SegmentedControl fill used by the header tab bar) instead of `dark-9`,
mirroring light mode where the body and the tab bar share `gray-1`.
Cards (`gray-9`) still sit a step above the body, and the contrast audit
stays at zero AA violations on the slightly lighter background.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* web: enforce the a11y gate + fix modal-overlay heading-order (review #1679)
Address the code review:
- Flip the Storybook a11y addon from test: 'todo' to 'error' so axe
violations fail the play-function tests in CI — the zero-violation
state is now gated, not point-in-time.
- Enforcing it surfaced a real heading-order skip: the Tools screen's
h4 'Tools'/'Results' sit under an h2 Modal.Title when a sampling/
elicitation request modal opens over them. Lift both to h3 (size h4
preserves the look) so h2→h3 is valid in any DOM order.
- Comment the intentional color+c pairing on the Remove/Disconnect
danger buttons.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* web: brighter warning alerts + pin the elicitation form's actions (#1678)
- Add a 'warning' Alert variant: a bright light-yellow fill (yellow-1)
with crisp near-black text and a bright yellow-5 left accent, instead
of the muddy brown a tinted yellow alert produced once
yellow-light-color was darkened to yellow-9 for contrast. Dark mode
keeps the light-on-dark-tint treatment. Applied to all five yellow
warning/notice alerts.
- Elicitation form: scroll only the form fields (capped ScrollArea),
keeping the quoted message, warning, and Cancel/Decline/Submit buttons
in view. The modal has no other close affordance, so the actions must
never scroll out of reach.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* web: robustly pin the elicitation form's actions (#1678)
The previous approach capped only the fields' ScrollArea with a fixed
rem reserve for the pinned chrome, which underestimated it on some
viewports — the whole modal then overflowed and the buttons clipped
half-off-screen. Instead cap the whole panel below the modal height and
lay it out as a flex column: the message and the warning + buttons are
pinned (flex: 0 0 auto) and only the fields (flex: 1) shrink and scroll.
Verified the buttons stay within the panel and the modal fits the
viewport down to very short heights.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* web: fix elicitation form scrolling — fields scroll, actions pinned (#1678)
The prior fix capped the panel with a bare max-height, but a flex:1
ScrollArea's viewport won't bound against a max-height (not fixed-height)
parent, so a tall form's fields overflowed unscrollably (clipped) rather
than scrolling. Adopt ToolDetailPanel's pattern instead: the modal gives
its body a bounded flex column, the panel fills it, and the fields use a
flex: "0 1 auto" ScrollArea — which sizes to content when the form is
short (modal stays compact, nothing scrolls) and shrinks + scrolls once
the panel is capped at the modal height, keeping the message, warning,
and Cancel/Decline/Submit pinned in view. Add a ManyFields story (with a
bounded-height decorator mirroring the modal) to cover the scroll case.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* web: fix elicitation scrolling in the real modal (cap fields, not flex)
The flex-column approach (panel flex:1 in a flex-column modal body, fields
flex:"0 1 auto") worked in a fixed-height Storybook decorator but NOT in the
real modal: the modal body is bounded by max-height, not an explicit height,
so the fields' ScrollArea viewport still couldn't bound — a tall form's
fields overflowed and pushed the warning + Cancel/Decline/Submit off-screen.
Drop the flex plumbing (and the modal-body styles) and cap the fields
directly with ScrollArea.Autosize mah=calc(90dvh - 22rem): it sizes to
content when the form is short (modal compact, nothing scrolls) and scrolls
once the fields hit the cap. The 22rem reserve = the measured non-field
chrome (modal header/padding + message + warning + buttons + gaps), so the
whole modal stays within 90dvh and the actions are always in view. Verified
in the real PendingClientRequestModal (new ElicitationFormTall story) for
both short and tall forms.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* web: pin the sampling modal's actions too (#1678)
Apply the elicitation-form treatment to SamplingRequestPanel: wrap the
request/response content in a ScrollArea.Autosize capped at
calc(90dvh - 10rem) so it scrolls while the Reject / Send Response buttons
stay pinned in view (the modal has no other close affordance). The 10rem
reserve is the pinned chrome (modal header/padding + the actions + gaps) —
smaller than the elicitation form's 22rem since there's no warning banner.
Add a SamplingTall story (8 messages + preferences) to cover the scroll
case in the real PendingClientRequestModal.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* web: sandbox hardening — opaque-origin inner frame, srcdoc-only, frame-ancestors
Harden the MCP Apps sandbox isolation model:
- The inner (untrusted-widget) iframe is sandboxed WITHOUT `allow-same-origin`
(default "allow-scripts allow-forms") and any server-supplied sandbox value
has `allow-same-origin` stripped case-insensitively, so the widget runs under
an opaque origin and cannot reach the proxy's DOM to bypass its own CSP via
the parent realm.
- Widget delivery is `srcdoc`-only (no document.write), which respects the
opaque origin.
- The relay accepts the inner frame's "null" origin with
`event.source === inner.contentWindow` as the unforgeable identity check.
- sandbox-controller.ts serves the proxy with
`Content-Security-Policy: frame-ancestors http://127.0.0.1:* http://localhost:*`;
fetch directives are deliberately omitted so a srcdoc iframe's inherited
policy container does not override the per-app CSP.
Updated the isolation-model comments and the sandbox-controller integration
test asserts the frame-ancestors header (and the absence of fetch directives).
Closes#1565
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BrihGWcrM9JGRyu41nzZYw
* web: enforce per-app _meta.ui.csp in the app bridge + surface resource-read failures
- createAppBridgeFactory now builds each app's `_meta.ui.csp` host-side via the
sandbox-csp library (approveCspSources → buildSandboxCspPolicy) and wraps the
untrusted widget HTML with wrapSandboxedHtml so the CSP <meta> is always the
first <head> child before the HTML reaches the sandboxed iframe. The approved
(post-filter) csp + permissions are echoed back via hostCapabilities.sandbox
so the view sees what was granted. The csp is no longer sent inline through
sendSandboxResourceReady — it is enforced via the wrapped meta.
- A per-app copy of HOST_CAPABILITIES prevents the sandbox echo from mutating
the shared constant.
- New optional `onResourceError` dep on AppBridgeFactoryDeps routes resource
read/post failures to the caller (also always console.error'd) instead of
leaving a silent blank frame.
Closes#1566
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BrihGWcrM9JGRyu41nzZYw
* web: full hostContext delivery + live host-context-changed updates
- createAppBridgeFactory now seeds the full hostContext via
snapshotHostContext(iframe, HOST_AVAILABLE_DISPLAY_MODES): theme, styles
(Mantine design tokens), displayMode ("inline"), availableDisplayModes, and
containerDimensions — replacing the theme-only seed. The local currentTheme
helper is dropped in favor of hostContext.ts.
- AppRenderer gains `displayMode` and `containerRef` props and pushes live
host-context changes via the SDK's sendHostContextChange (partial
host-context-changed notifications), never a hand-maintained snapshot:
- theme + styles via a MutationObserver on <html data-mantine-color-scheme>,
- containerDimensions via a ResizeObserver on the host container (or the
iframe fallback), gated on `initialized`, skipping 0x0 and value-equal
repeats, plus a one-shot push on initialize once layout settles,
- displayMode whenever the prop changes (Maximize/Restore), gated on init.
Adds AppRenderer host-context tests (theme flip incl. resolved styles,
displayMode push + init gate, container-dimensions on init and resize, observer
teardown) and updates the factory + story mocks for the new hostContext shape
and sendHostContextChange method.
Closes#1567
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BrihGWcrM9JGRyu41nzZYw
* web: honor ui/notifications/size-changed and ui/request-display-mode
AppRenderer:
- New `onSizeChange` prop forwards the view's ui/notifications/size-changed
reports (via the bridge `sizechange` event).
- New `onRequestDisplayMode` prop backs an `onrequestdisplaymode` bridge handler
that returns the mode the host actually applied; with no handler the request
is declined by returning the current host-side mode (falling back to inline).
AppsScreen:
- Adds a host-controlled RendererContainer (measured for containerDimensions)
with an inner box sized by the view-reported height (`appHeight`), so
host→view container size and view→host size-changed don't couple.
- `handleSizeChange` records the reported height (ignored while maximized);
`displayMode` is derived from the maximize toggle and pushed to the view;
`handleRequestDisplayMode` filters against HOST_AVAILABLE_DISPLAY_MODES
(declining e.g. "pip") and maps fullscreen↔inline onto the maximize state.
Adds AppRenderer tests (size forwarding, request-display-mode apply/decline/
fallback) and AppsScreen tests (frame sizing on report, no-height ignore,
fullscreen/restore/decline via request-display-mode). Story mocks gain
sendHostContextChange/addEventListener.
Closes#1568
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BrihGWcrM9JGRyu41nzZYw
* web: honor ui/download-file with confirmation and http(s) allowlist
createAppBridgeFactory now advertises the `downloadFile` host capability and
attaches an `ondownloadfile` handler:
- Requires a host-mediated confirmation (window.confirm) listing the requested
file(s); a declined prompt or an empty payload returns isError without acting.
- Inline EmbeddedResources (text or base64 blob) are written to disk via the
shared downloadBlob object-URL anchor; base64 is decoded via base64ToBytes.
- ResourceLinks are opened in a new tab, restricted to http(s) by the shared
isHttpUrl allowlist (javascript:/data:/file:/malformed are rejected).
- Labels shown in the confirmation are sanitized (control + format chars
stripped, length clamped) so a server-supplied filename/URI can't inject
newlines or reflow the prompt.
- Partial-batch success is surfaced (isError only when nothing downloaded), with
skipped items warned.
Adds factory tests for the full download surface (text/blob/link, decline,
empty, throw, non-http rejection, label sanitization/clamping, partial batch).
Closes#1569
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BrihGWcrM9JGRyu41nzZYw
* web: surface ui/message and app logging in panels below the running app
AppRenderer:
- New `onMessage` prop backs an `onmessage` bridge handler that surfaces the
view's ui/message content and returns the spec-required empty result;
with no handler the submission is declined (isError).
- New `onLog` prop forwards MCP `notifications/message` log entries via the
bridge `loggingmessage` event, honoring the advertised `logging` capability.
AppsScreen:
- Adds `messages`/`appLogs` state (cleared with the reported height on
select/open/close/back via resetAppChannels) and `handleMessage`/`handleLog`.
- Renders a "Messages from app" panel (data-testid="apps-messages", reusing
ContentViewer) and a default-expanded collapsible "App logs" panel
(data-testid="apps-logs", reusing LogLevelBadge) with logger names and a
Clear button, both as pinned panels below the running widget.
Adds AppRenderer tests (log forward + no-handler no-throw, ui/message
route/empty-result/decline) and AppsScreen tests (message log render + empty
result + clear-on-close, log panel expand/collapse/clear).
Closes#1570
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BrihGWcrM9JGRyu41nzZYw
* web: stage partial input — replay tool-input-partial before final tool input
AppRenderer:
- New `partialInputs` prop (ordered fragments), snapshotted into pendingPartials
at bridge-build time (read via a ref so prop churn never rebuilds the iframe)
and cleared on dispose. flushPending replays them via
bridge.sendToolInputPartial BEFORE the complete tool-input, per spec.
AppsScreen:
- New `partialStages` state + `handleStagePartialInput` (snapshots current form
values). resetAppChannels gains a `keepPartials` option so Open App preserves
the staged fragments (they're consumed by the renderer) while select/close/
back clear them.
- Adds a "Stage partial input" control (fielded apps only) with a staged count
and "Clear staged", wired to AppRenderer's `partialInputs`.
Adds AppRenderer tests (ordered replay before tool-input; none when omitted) and
AppsScreen tests (stage/count/clear gated on fields; staged partials survive
Open and are replayed).
Closes#1571
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BrihGWcrM9JGRyu41nzZYw
* web: app lifecycle status + machine-readable data attributes on the Apps screen
AppRenderer:
- New AppRendererStatus ("loading" | "ready" | "error") + onAppStatusChange
callback. Fires "loading" at the start of every (re)build, "ready" on the
view's initialized signal, and "error" on a factory throw/rejection.
AppsScreen:
- appStatus/appError state; handleAppError captures the reason locally and
forwards to the parent onError. resetAppChannels clears both.
- Surfaces the contract as data-testid="apps-form" carrying data-app-status and
data-app-error, renders an error panel (data-testid="apps-error") in place of
the blank frame, and tags the stage button data-testid="apps-stage".
- AppDetailPanel's Open App button gains data-testid="open-app".
App.tsx:
- Wires the bridge factory's onResourceError to a red toast so a malformed/404
UI resource is no longer console-only (the renderer separately drives
data-app-status, so a driver times out on never-reaching-ready and reads it).
Documents the data-testid/data-* automation contract in clients/web/README.md.
Adds AppRenderer status-transition tests and AppsScreen status/error-panel
tests.
Closes#1572
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BrihGWcrM9JGRyu41nzZYw
* review: warn when server sandbox drops allow-scripts + revert incidental lockfile churn
Addresses the claude[bot] review on PR #1650:
- sandbox_proxy.html: log a console.warn when a server-supplied `sandbox`
value lacks `allow-scripts` after the `allow-same-origin` strip, so a
fail-closed (app-never-runs) sandbox is diagnosable instead of silent.
- Restore clients/{cli,launcher,tui,web}/package-lock.json to their
v2/main state — the earlier churn (@emnapi/@napi-rs optional deps, a
dropped version field) was incidental `npm install` regeneration
unrelated to the sandbox change.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BrihGWcrM9JGRyu41nzZYw
* review: document CSP-vs-permissions filtering asymmetry (PR #1652)
Address the claude[bot] review's finding #2: add a comment explaining why
`csp` is injection-filtered (approveCspSources) while `permissions` is passed
through verbatim — permissions is a structured boolean object consumed only by
the sandbox proxy's buildAllowAttribute() (fixed key→token mapping), so it
never reaches the iframe sandbox/allow attribute as raw text. No logic change.
Also merges v2/1565-sandbox-hardening (review fixes) up the stack.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BrihGWcrM9JGRyu41nzZYw
* test: lock in per-app HOST_CAPABILITIES isolation (PR #1652 round 2)
Add a test asserting the shared HOST_CAPABILITIES.sandbox stays undefined after
a sandboxready run sets hostCapabilities.sandbox — guards against a silent
regression if the per-app spread copy were ever dropped.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BrihGWcrM9JGRyu41nzZYw
* review: gate theme observer on initialized + document seed/dep assumptions (PR #1654)
Address the claude[bot] review:
- Finding #1 (applied): gate the theme+styles MutationObserver on the view's
`initialized` signal, matching the container/displayMode pushes — a theme
flip in the construction→handshake window no longer races ui/initialize. The
factory seeds the construction-time theme, and the first post-init flip
carries the current value. Added tests: pre-init flip is dropped; the live
flip test now completes the handshake first.
- Finding #2 (applied, comment): note in snapshotHostContext that the
`displayMode: "inline"` seed assumes inline-at-open (AppsScreen always opens
inline; #1568's displayMode push carries transitions).
- Finding #4 (applied, comment): explain why `containerRef` is a build-effect
dep yet doesn't force a rebuild (sameInputs ignores it; read lazily).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BrihGWcrM9JGRyu41nzZYw
* review: guard 0-height size reports, distinct RendererFrame, shared payload type (PR #1656)
Address the claude[bot] review:
- #1 (applied): handleSizeChange ignores a non-positive height — a view's
ResizeObserver can transiently fire 0 pre-layout/teardown, which would
collapse the frame; mirrors AppRenderer's 0×0 skip. Added a test.
- #2 (applied): reinstate a distinct `RendererFrame` constant for the inner
content-sized box so its role reads clearly vs. the outer RendererContainer.
- #3 (declined + comment): keep carrying `appHeight` across maximize→restore —
clearing it would flash the frame to full-card height until the next
size-changed; the carry-over restores at the prior size. Documented.
- #4 (applied): type handleSizeChange's param as AppBridgeEventMap["sizechange"]
so the payload shape has a single source of truth with AppRenderer's prop.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BrihGWcrM9JGRyu41nzZYw
* review: distinguish links, cap batch size, comment truncation (PR #1657)
Address the claude[bot] review:
- #1 (applied): the confirmation now reads "download or open N item(s)" and
resource_links are prefixed "↗" in the summary, so a link that opens in a tab
is distinguishable from an embedded file that saves to disk.
- #2 (applied): cap a single ui/download-file batch at MAX_DOWNLOAD_ITEMS (20);
an oversized batch is rejected (isError) before the prompt, with a warn.
- #3 (declined): can't check window.open's return value to detect a blocked
popup — we pass `noopener`, and per spec window.open returns null even on a
successful noopener open, so null can't distinguish blocked from succeeded.
- #4b (applied, comment): note the start-of-label truncation is intentional so a
link's scheme+host stays visible for the trust decision.
Adds tests: oversized batch rejected without confirming/acting; resource-link ↗
prefix in the summary.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BrihGWcrM9JGRyu41nzZYw
* review: skip embedded resource with neither text nor blob (PR #1657 round 2)
Address the round-2 observation: an untrusted EmbeddedResource carrying neither
`blob` nor a string `text` previously produced `new Blob([undefined])` — a file
containing the literal text "undefined", counted as success. Now such an item
is skipped (returns false, like a rejected link), so isError reflects it. Added
a test asserting no object URL is created and the batch reports isError.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BrihGWcrM9JGRyu41nzZYw
* review: type-honest formatLogData, cap channels, log-panel a11y (PR #1658)
Address the claude[bot] review:
- #1 (applied): formatLogData coalesces JSON.stringify(undefined) → "" so the
`: string` return type is honest for a data-less log.
- #2 (applied): cap retained messages/logs at MAX_APP_CHANNEL_ENTRIES (500) via
appendCapped — a chatty widget no longer grows the panels/DOM without bound
between Clear/close; oldest entries drop.
- #3 (declined): keep the Messages panel without a Clear/collapse — messages are
sparser and already cleared on close/switch; keeping it simple.
- #4 (applied): the App-logs toggle now has aria-controls pointing at the
Collapse region (id="apps-logs-region") for assistive tech.
Adds tests: no-data log renders (coalesce branch), 501 logs cap at 500 (oldest
dropped), aria-controls assertion.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BrihGWcrM9JGRyu41nzZYw
* review: stable id key for Messages panel (PR #1658 round 2)
Address the round-2 nit: give AppMessage a stable `id` (assigned via
nextMessageIdRef in handleMessage) and key the Messages panel on it instead of
the array index — so the appendCapped front-drop can't renumber keys once a run
exceeds the cap, matching AppLogEntry's treatment.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BrihGWcrM9JGRyu41nzZYw
* review: note partialStages intentionally survives handleOpen (PR #1659)
Address the claude[bot] review's observation #2: add a comment at the
handleOpen call site clarifying that partialStages is intentionally NOT cleared
there — AppRenderer snapshots the fragments into its own pendingPartialsRef at
build time, and the staging UI only renders while not running, so the surviving
state is invisible until the next reset drains it.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BrihGWcrM9JGRyu41nzZYw
* review: align error-panel docs with behavior + gate data-app-error on running (PR #1660)
Address the claude[bot] review:
- #1 (applied, docs): the error panel renders BELOW the (blank) frame, not in
place of it — the renderer stays mounted so a factory error still shows the
reason alongside the iframe (gating the frame off on error would drop the
auto-launched iframe an integration test relies on). Reworded the README row
and the test comment to say "below the frame"; added a code comment.
- #2 (applied): gate data-app-error on `running` (like data-app-status) so the
two attributes can't desync if a future path sets an error without running.
- #3 (no change): resource-read failures staying at data-app-status="loading"
(toast only) is intentional and documented.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BrihGWcrM9JGRyu41nzZYw
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Three related "fill the available height" layout fixes to the connected screens:
- Tools & Prompts sidebars: replace the fixed magic max-height on ToolControls/
PromptControls with the Resources pattern — sidebar Card `variant="sidebar"`
(full-height flex column) + `Stack flex={1} mih={0}` + a flex-filling
ScrollArea — so the list runs to the bottom of the card before scrolling.
- Logs/Console/Protocol/Network standalone screens: only override ScreenLayout
`h` when embedded. Passing `h={undefined}` clobbered the default
`h: calc(100dvh - header)` (withProps plain-spreads), collapsing an empty
screen to its controls' height. Also center the Network/Protocol empty states.
- Tools placeholder: render the "Select a tool" empty state in a full-height
`DetailCard flex={1}` (like Prompts/Resources) instead of the content-sized
preview card, so it fills the pane.
Claude-Session: https://claude.ai/code/session_015sMW1YZXkM7fDkdPtHpE95
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(web): single header MonitoringToggle + Prompts-style Tools results (#1661)
Consolidate the monitoring column's open/close affordances into one header
toggle, and align the Tools/Prompts result panes and the sidebar log layout.
- Add a single `MonitoringToggle` in the header, to the right of the theme
icon. It opens/closes the monitoring column and only shows when a server is
connected OR a connect attempt failed (wide viewport + a monitor tab
available) — the same availability rule the column uses.
- Remove the scattered pin/close icons: delete `PinColumnButton`; drop `onPin`
from the Logs/Protocol/Network/Console screens and their stream panels; drop
`onOpenMonitor` from the server list; remove the close button from
`MonitoringControls`/`MonitoringScreen`.
- Tools screen: remove the "Results will appear here" placeholder pane. Results
now replace the input form in a single content pane (the Prompts pattern),
with a top-left X (ToolResultPanel) that dismisses them back to the form;
form values are preserved for a re-run.
- Prompts screen: the argument form fills the content pane width (drop the 40%
cap), matching the Tools input.
- LogEntry: add a `compact` two-line layout (time/level/logger on line one,
message wrapping below) used by the embedded monitoring-column Logs.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015sMW1YZXkM7fDkdPtHpE95
* polish(web): tooltips on icon buttons + rename "monitoring column" → "monitoring sidebar" (#1661)
- Add Mantine `Tooltip`s to the icon-only controls: Client settings, Theme
(dynamic light/dark label), the monitoring-sidebar toggle, the list-level and
per-entry Expand/Collapse toggles, the Pin/Unpin toggle, and the Replay
button. Labels mirror each control's existing aria-label.
- Rename the feature term "monitoring column" → "monitoring sidebar" across the
user-facing labels (Open/Close/Resize), comments, and tests, so the UI calls
it a sidebar consistently.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015sMW1YZXkM7fDkdPtHpE95
* review: consistent sidebar wording + reuse MonitoringToggleProps (#1661)
Address the two optional notes from the PR #1662 review:
- Make the MonitoringToggle JSDoc voice consistent ("sidebar" throughout,
no lingering "column").
- Drop ViewHeader's duplicate `MonitorToggleProps` and reuse the element's
exported `MonitoringToggleProps` as the single source of truth.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015sMW1YZXkM7fDkdPtHpE95
* feat(web): slide-in/out animation for the monitoring-sidebar toggle (#1661)
The header MonitoringToggle now animates in/out via a Mantine Transition whose
in/out styles drive width + margin-left (plus opacity/translateX): as its slot
grows from 0 → the 36px icon, the flex-end header row reflows and pushes the
theme + client-settings buttons one slot to the left; the exit reverses and they
slide back over. A negative margin-left while collapsed cancels the row gap so
the theme icon stays flush-right when the toggle is absent. Only appearance/
disappearance (connect / disconnect / narrow viewport) animates — opening or
closing the sidebar leaves the toggle in place.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015sMW1YZXkM7fDkdPtHpE95
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Address PR #1641 review feedback:
- Add readInspectorVersionSafe() (non-throwing) in core/node/version.ts and use
it in the web backend, so a failure to resolve the root manifest hides the
(cosmetic) version badge instead of crashing the backend at startup. Covered
by tests for both the resolved and unresolvable paths.
- Update MonitoringScreen's docstrings: it now wraps every supplied screen in a
ScreenStage (only the active tab mounts, cross-fading) rather than rendering
just the active tab's node.
The third fetch-consolidation suggestion (one useInitialConfig instead of three
GET /api/config hooks) is tracked as a follow-up in #1643.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BrihGWcrM9JGRyu41nzZYw
Make the server-list toolbar's expand/collapse toggle and open/close-monitoring
icons the same width and spacing as the header's theme / client-settings icons:
- ListToggle (default variant), PinColumnButton, and the MonitoringControls
close button now render as `ActionIcon variant="subtle" size={36}` (glyph 20),
matching the header's 36px ActionIcons, instead of wider `Button`s.
- ServerListControls uses `gap="sm"` so its buttons sit the same 12px apart as
the header's RightSection controls.
Update a TaskListPanel test that found the toggle by a brittle
`mantine-Button-root` class to select it by its accessible name instead.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BrihGWcrM9JGRyu41nzZYw
When a server is connected, has monitor screens to show, and the monitoring
column isn't open, show an "open monitoring column" button to the right of the
list expand/collapse toggle on the server list. It reuses PinColumnButton (the
same right-sidebar affordance the monitor screens use, now with an optional
label), so opening the column is reachable from the server list — not only from
a monitor screen's pin button. InspectorView gates it on
`canPin && monitorAvailable.length > 0 && !effectivePinned`.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BrihGWcrM9JGRyu41nzZYw
Extract the primary pane's per-screen enter/exit transition into a shared
`ScreenStage` element (the `fade-up` Mantine Transition: incoming screen slides
up + fades in while the outgoing fades down + out, both absolutely positioned).
InspectorView now imports it (behavior unchanged), and the pinned monitoring
column uses it too — via a new `fill` variant that stretches the stage to the
column's height — so switching Logs/Protocol/Network animates the same way as
switching the main screens.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BrihGWcrM9JGRyu41nzZYw
Long request URLs stretched every Network card to their own max-content
width, bleeding the cards (and their status badges) past the pinned
monitoring column. Mantine's ScrollArea `content` slot defaults to
`min-width: max-content`, so a non-wrapping URL forced the whole list wider
than its viewport.
Add an opt-in `constrainContentWidth` prop to EmbeddableScrollArea that
relaxes that slot to `min-width: 0`, so the list can't grow past its
viewport and each row manages its own overflow — the URL already lives in a
horizontal ScrollArea, which now actually scrolls. Scoped to the Network
stream panel (Logs/Protocol rows wrap or truncate, so they keep the default
where a long line scrolls the list).
Verified in Storybook (embedded 380px column): cards clamp to the viewport,
no list/page horizontal overflow, and the URL scrolls within its own area.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BrihGWcrM9JGRyu41nzZYw
- Direction labels now both point right so the arrow reads clearly at
badge size: incoming "client ← server" becomes "server → client"
(from → to), in MessageDirectionBadge and MessageDirectionFilter
(shared by the Protocol and Network views).
- The Protocol list's pinned section header now reads "Pinned Messages"
(was "Pinned Requests"), matching the "Messages" panel title.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BrihGWcrM9JGRyu41nzZYw
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
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
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
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
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
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>
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>
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>
- 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>
- 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>
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>
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>
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>
- App.tsx: replace the inline `style={{}}` on the re-auth banner wrapper with
Mantine style props (pos/top/bg) plus a `.reauth-banner-bar` class for the
z-index/box-shadow that have no Mantine shorthand.
- App.tsx: extract the ~100-line inline `onAuthorize` and the inline `onCancel`
step-up handlers into named `handleStepUpAuthorize`/`handleStepUpCancel`.
- ReAuthBanner: move the flat `styles={{ root }}` (background/border) into a
`reauth` Alert theme variant; replace the raw `<span>` with `<Text>`.
- StepUpAuthModal: extract the inline `onClick` wrapper into `handleAuthorize`.
No behavior change; validate + integration green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DZyeWManYTvcXxjcrhxSQc
The "cancels a pending debounce timer when the input is re-focused" test
passed trivially after the delay:null change: synchronous typing meant the
assertion ran at t≈0, before the 300ms debounce could ever fire, so it
passed whether or not handleFocus's clearTimeout worked.
Rewrote it with fake timers so the debounce window elapses deterministically
(advance the clock by 400ms directly). The interaction is driven with
fireEvent rather than userEvent because userEvent's async internals deadlock
under vitest fake timers with the Mantine/happy-dom stack. Verified
load-bearing: removing the clearTimeout in handleFocus makes it fail
(expected 1 to be 0), and it passes once restored.
Also tightened the sibling-context comment (with delay:null the sibling is
reliably "es"); the tolerant /^es?$/ regex is left as-is.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S3fTN8H3R8YV4yUGvZjYnX
Root-cause and deterministically fix the recurring 5s-timeout flakes that
surfaced under v8-instrumented, concurrent load. All fixes are test-only; no
product source changed. No test was skipped or disabled, and the per-file
coverage gate (>=90 on all four dims) still holds.
Common root cause across the modal/form/screen suites: userEvent.setup()
schedules a real setTimeout between keystrokes. Under CPU contention those
yields balloon, so multi-field interactions blew past the 5s per-test
timeout. Fix: userEvent.setup({ delay: null }) removes the per-keystroke
real-timer dependence (typing dispatches synchronously), making the suites
load-independent.
Per file:
- ServerConfigModal / ServerImportConfigModal / ServerImportJsonModal /
ResourcesScreen / InspectorView: userEvent.setup({ delay: null }).
- PromptArgumentsForm (completions): replaced wall-clock sleeps sized just
past the 300ms completion debounce (await setTimeout(400)) — which race
the debounce timer under load — with awaited conditions (findBy/waitFor)
on the real rendered outcome, plus delay:null. Negative assertions now
lean on the component's synchronous state/timer teardown instead of a
timed window, so they are deterministic regardless of machine speed.
- inspectorClient integration "tracks stderr logs": the child's stderr is
piped out-of-band from the tool's JSON-RPC response, so reading the log
synchronously after callTool raced the stderr chunk. Wrapped the assertion
in vi.waitFor so it polls until the line lands.
Verified: web validate + 6x test:coverage (4 sequential + 2 concurrent
under load) + standalone test:integration, all green (229 files / 3237
tests per coverage run; 799 integration tests), zero intermittent failures.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S3fTN8H3R8YV4yUGvZjYnX
* feat(web): add View Source toggle to resource preview (closes#1552)
Markdown, CSV, and HTML resources render in a way that hides the
underlying text (react-markdown, the CSV table, the sandboxed HTML
iframe). Add a "View Source" button to the resource preview footer, left
of Refresh and styled to match it, that swaps the rendered preview for
the raw resource text. In source mode the button reads "View Rendered".
- Only shown for markdown/csv/html (other kinds already show their text,
or have no meaningful text source).
- Source mode forces ContentViewer through its plain-text renderer; the
displayed MIME label keeps the resource's real type.
- The toggle resets to the rendered view when the previewed resource
changes (React's adjust-state-during-render pattern — no effect).
Tests cover the toggle round-trip, per-kind visibility, and the reset;
a Storybook play function exercises rendered → source → rendered.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(web): address View Source review nits (#1553)
- Per-item source gating: in source mode only source-toggleable items
switch to plain text, so a mixed multi-part resource doesn't force an
image/PDF blob through the text decoder.
- Share a FooterButton constant between Refresh and the View Source
toggle so the two stay visually identical (per the subcomponent
guidance — both had the same two inline props).
- Add aria-pressed to the toggle so assistive tech gets explicit on/off
state beyond the label flip.
Tests added for the aria-pressed state and the mixed multi-part case.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(web): expand ContentViewer to render PDF, CSV, HTML, XML, JSON, CSS (closes#1329)
Turn ContentViewer into a per-MIME dispatch surface for resource content.
It now accepts raw TextResourceContents | BlobResourceContents (in addition
to the existing ContentBlock path), so ResourcePreviewPanel.toContentBlock
collapses to a passthrough and blob-typed resources render natively instead
of falling back to opaque "[Binary content …]" text.
New renderers (clients/web/src/components/elements/):
- CodeHighlight: lazy react-syntax-highlighter prism-light element. Runtime,
tomorrow theme, and each grammar dynamic-import on first use (one chunk per
grammar). Module-level registeredLanguages / failedLoads / loadingPromises
caches; plain Code block until a grammar resolves.
- PdfFrame: iframe over a Blob URL (#view=FitH), revoked on unmount.
- CsvTable: papaparse -> Mantine Table (first 100 rows), plain-Code fallback.
- HtmlFrame: sandbox="" iframe over a Blob URL with an injected CSP meta
(no script-src, falls through to default-src 'none').
- contentViewerUtils: getMimeKind, base64 decode helpers, formatXml,
wrapHtmlWithCsp, SAFE_HREF markdown-anchor allowlist (unsafe href -> span).
ResourcePreviewPanel: extend the URI-suffix MIME inference table
(.csv/.json/.xml/.html/.htm/.css/.pdf alongside .md).
Deps: papaparse + react-syntax-highlighter (+ @types). tsconfig paths stubs
redirect the deep RSH imports for typecheck (the @types ambient declarations
are shadowed by the real .js under bundler resolution) — same pattern the
repo uses for react/pino.
Tests cover PDF blob-URL creation + cleanup, CSV table render + fallback,
HTML sandbox attributes + CSP injection, JSON/XML/CSS highlighted output,
the lazy-load/alias/failed-load/shared-runtime paths, and the URI-suffix
MIME-inference paths. validate, test:coverage (per-file gate), test:storybook,
and integration all pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(web): harden ContentViewer blob/CSV rendering per review (#1551)
Address review feedback on the ContentViewer MIME-dispatch PR:
- useObjectUrl: fix a dev-only StrictMode bug where the committed object
URL was revoked but never recreated (setup→cleanup→setup with no
re-render), blanking PDF/HTML previews. Revocation is now deferred to a
microtask and guarded by a "live URL" ref so a StrictMode re-setup
cancels it, while a real unmount / blob change still releases the URL.
Avoids set-state-in-effect; mirrors AppRenderer's deferred-disposal.
- atob: guard base64 decoding with tryDecodeBase64ToUtf8 /
tryDecodeBase64ToBytes so a malformed blob from an external server
degrades to the binary-content notice instead of throwing during render
and taking down the whole preview panel.
- CsvTable: surface the 100-row display cap with a
"Showing first N of M rows" caption (no silent truncation).
- Extract BinaryNotice into its own module so PdfFrame can reuse it
without an import cycle.
Tests added for the StrictMode path, malformed-base64 fallbacks (PDF +
textual blob), the safe-decode helpers, and the truncation caption.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Follow-up to #1545. The close-block only fired for an invalid issuer, so an
EMA config enabled with a valid issuer but a blank client ID was still silently
dropped on close by the persist gate — the same no-feedback bug, different
field.
- validateClientSettings gains a requireComplete mode that also flags the
required IdP fields (issuer, clientId) when blank, with distinct per-field
messages. Inline (as-you-type) validation is unchanged — required-but-blank
errors only surface on reveal, so the form never nags about not-yet-filled
fields.
- canPersistClientSettingsDraft is redefined in terms of that mode, so the
persist gate and the close-time field errors share one source of truth.
- ClientSettingsModal.handleClose now blocks + reveals on ANY require-complete
error (invalid issuer OR blank required field) and keeps the modal open. The
form prop revealIssuerError becomes revealErrors and surfaces the client-ID
error too. Escape hatch: disable enterprise IdP (an empty issuer no longer
closes, since that's itself incomplete).
Tests cover the blank-clientId close path, fix-each-field, and disable-EMA
escape, plus requireComplete validation.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(web): update OAuth E2E tests to use new authentication method
- Replaced `runGuidedAuth` with `authenticate` in the InspectorClient OAuth end-to-end tests to support quick authentication with CIMD pre-registration.
- Updated comments to reflect the change in authentication method and its support for HTTP test metadata URLs.
- Ensured that the tests continue to validate the expected authorization URL after the change.
Additionally, imported `ensureCimdClientRegistration` in the OAuthManager to streamline client registration during the authentication process.
* Guided-auth removal, OAuth smoke testing, and TUI/CLI support
* Added CLI OAuth runner flags tests
* Added clarifying comment on port 0 binding
* README notes on TUI oauth callback port, prettier fixes for CLI tests
* CIMD validation uses inline like EMA IdP
* Fixed TUI test failures from merge
* test: bring 5 OAuth files over the 90 per-file coverage gate
The guided-auth-removal work added/touched several files that fell below
the repo's >=90 per-file coverage gate (npm run coverage). CI doesn't run
the gate (it's local-only), so validate + test:integration passed while
these gaps slipped through. Bring them all back over the line:
- core/auth/node/runner-oauth-callback.ts: test bad-URL/non-http-scheme
throws, no-port->80 default, and IPv6 hostname bracketing. Annotate the
provably-dead guards (empty host, out-of-range port, String(err)
fallback, pathname "/" fallback) -- all rejected by new URL() upstream --
with justified v8 ignore comments instead of lowering the gate.
- core/auth/cimd.ts: test the AS-doesn't-support-CIMD early return.
- core/react/useClientSettingsDraft.ts: test the functional-updater path,
the stale-onChange-after-close prev===null guard, and the
timer-fires-after-close value===null guard.
- ClientSettingsForm.tsx: test the CIMD checkbox toggle and CIMD URL clear.
- clients/tui/src/utils/oauthDisplay.ts: test static/dcr registration
kinds, the expired IdP session, and the configured-scope/empty-scope
branches of formatScopes.
npm run coverage now passes (exit 0) across web, cli, tui, launcher.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: cliffhall <cliff@futurescale.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(web): defer IdP issuer URL error to onBlur in Client Settings (#1544)
The Enterprise-Managed Authorization Issuer field validated its URL inline on
every render, so the 'Must be an http(s) URL' error nagged while the user was
still mid-typing (e.g. before 'https:/…' is complete).
Gate the issuer error display on an issuerTouched flag set onBlur; once
touched it tracks the value live, so the error clears as soon as a valid URL
is entered. The persist gate (canPersistClientSettingsDraft → validateClientSettings)
is unchanged and still validates independently, so an invalid issuer is never
written to the backend regardless of touched state.
Closes#1544
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(web): reveal issuer error and block close on invalid issuer (#1544)
Extend the on-blur deferral so an invalid issuer can't be silently dropped on
modal close (the auto-save flush would otherwise discard it with no feedback).
ClientSettingsModal now intercepts its close (X / Esc / overlay): if the issuer
is invalid it reveals the field error (via the form's revealIssuerError prop,
overriding on-blur gating) and keeps the modal open instead of closing. The
user can fix the URL or clear the field — an empty issuer is valid to leave.
The block lives in the modal (unit-tested) rather than App, so App.tsx stays a
thin flush+close that only runs when the close is allowed; the persist gate is
unchanged. Reveal state resets via the modal's open/close remount key.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(core): require a real host in isAbsoluteHttpUrl, not just an http(s) scheme (#1544)
The issuer validation used URL.canParse + an http(s) protocol check, which the
WHATWG parser accepts for bare/degenerate values that aren't actually usable
URLs — e.g. https://foo, https://example, https://. , https://.. all passed.
Tighten the shared rule (used by both the Zod config schema and the form's
inline validation): after parsing and the http(s) scheme check, require the
host to actually look like a host — localhost, an IPv6 literal, or a dotted
domain/IPv4 with at least two non-empty labels. Single-label, bare-dot, and
empty-label hosts are now rejected. Add core tests for the accepted
(dotted/IPv4/IPv6/localhost) and newly-rejected cases.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(web): remove mantine-border-animate, use green highlight border (#1535)
The @gfazioli/mantine-border-animate dependency emitted a persistent
React "forwardRef render functions accept exactly two parameters"
warning on every page load. Remove the dependency entirely and replace
the freshly-added card's animated border with a green border.
- Drop @gfazioli/mantine-border-animate from clients/web (package + lock)
and remove its styles.css import from main.tsx.
- Add a `highlighted` variant to the Card theme that draws a 2px green
border, driven by a new --inspector-highlight-border token in App.css.
- ServerCard now selects the variant (disabled > highlighted) instead of
wrapping in BorderAnimate; the BorderAnimate flex-wrapper `w="100%"`
workaround is no longer needed.
- Update ServerCard/ServerListScreen tests to assert the highlighted
variant instead of the removed BorderAnimate DOM.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(web): drop redundant Card props on ServerCard (#1535)
ThemeCard.defaultProps already sets withBorder and padding="lg", so the
explicit props on ServerCard's <Card> were redundant. Addresses review
feedback on PR #1536.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(web): note highlighted Card variant relies on inherited withBorder (#1535)
Addresses a maintainability flag from the PR #1536 re-review: the
highlighted variant only sets borderColor/borderWidth and depends on the
withBorder:true default, so make that coupling explicit in a comment.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Enterprise-Managed Authorization "Issuer" field accepted arbitrary
non-URL text and only failed at save time, surfacing a raw Zod issue array
in "Failed to save client settings" toasts.
- Inline field-level validation: the Issuer input shows a friendly error
("Must be an http(s) URL, like https://idp.example.com").
- canPersistClientSettingsDraft gates on issuer validity (deferring to
validateClientSettings) so an invalid value is never sent to the backend.
- Extract isAbsoluteHttpUrl in core/client/config-parse so the Zod schema
and the form share one http(s)-only URL rule.
- Harden the fallback save-error toast via formatClientConfigLoadError.
Closes#1533
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(web): audit branch-coverage outliers, raise per-file gate to 70 (#1271)
The web per-file coverage gate had drifted red: the recently-landed EMA
auth feature (core/auth/ema/*) and ClientSettings components shipped
undertested, and the prior 50% branch floor let business-logic branches
go untested. CI does not run the coverage gate, so this accumulated.
Add tests to bring every failing file above the gate and push branch
coverage well past 70 across the board, then raise the per-file branches
threshold 50 -> 70 in clients/web/vite.config.ts.
- EMA core (emaFlow, resourceContext, jwt, wire): 50-88% -> 86-100% branch
- EMA OIDC/transport (idpOidc, tokenEndpoint, transportProvider): -> 97-100%
- oauthManager / discovery: 73-83% -> 88-92%
- ClientSettingsModal/Form, ConnectionInfoContent: 0-88% -> 96-100%
- useClientSettingsDraft / useEmaIdpLoginState hooks: 20% -> 100%
- TaskStatusBadge / SortableServerCard outliers: 50% -> 100%
Coverable branches reached by mocking useComputedColorScheme (dark theme),
dnd-kit useSortable (isDragging), and fetch/OIDC paths. Remaining sub-100
branches are genuinely-unreachable defensive guards or happy-dom-inherent
(portal / useMediaQuery / typeof window) and documented in test comments.
No source files changed. Global web branch coverage 88.73% -> 90.63%.
AGENTS.md updated; cli/tui/launcher stay at 50 (out of scope).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test: raise per-file coverage gate to 90/90/90/90 across all clients
Follows the #1271 web branch-coverage audit (50 -> 70) by raising the
whole per-file gate to >=90 on all four dimensions — lines, statements,
functions, branches — for web, cli, tui, and launcher (core/ is measured
under the web suite). A codebase-wide audit added real tests for every
outlier and brought global coverage up across the board.
Config: all four vitest/vite coverage configs set to 90/90/90/90.
Two pure-type modules (core/mcp/import/types.ts, clients/web/server/
types.ts) added to the web coverage exclude list, matching the existing
type-only exclusions.
Tests: ~50 test files extended/created across core (auth, ema, mcp,
remote, state, storage, react), web components/screens/server, and the
cli/tui clients. Highlights: full EMA OAuth flow + transport coverage,
inspectorClient/remote server backfill, every Mantine control's handler
branches, and the cli unsupported-method / argv-fallback paths.
Genuinely-unreachable branches are annotated at the source with justified
`/* v8 ignore -- <reason> */` comments rather than lowering the gate:
happy-dom-inherent paths (Mantine portal, useMediaQuery, typeof window
SSR guards), React StrictMode effect replay, and provably-dead defensive
guards (?? fallbacks the types make impossible, Select.onChange values
outside the allowed list). No runtime logic changed; the only structural
edits are two behavior-preserving refactors (ResourceTemplatePanel hoists
the field value to a local so the ignore applies in JSX; ToolsScreen
swaps a ternary for an if) and prettier line-wrapping.
Also fixed a flaky real-timer debounce assertion in PromptArgumentsForm
(asserted an exact sibling-typing interleaving that raced the 300ms
debounce; now asserts the load-bearing fact — a non-empty fire-time
sibling capture).
AGENTS.md updated to describe the uniform 90 gate and the v8-ignore
policy. All four client coverage gates, root validate, storybook (370),
and smoke pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(web): make connect-crash SSE reader stop on the terminal event
`readSseEvents` looped until the server closed the stream (`done`). The
server closes the events stream immediately after writing the terminal
`transport_error`, but under the heavy parallel scheduler pressure of a
full `coverage` run (v8 instrumentation across all workers) that close
can lag far enough behind the event to trip the 30s test timeout — a
hang, not a wrong result. This made the test intermittently fail under
`npm run coverage` (it passed in isolation and under CI's lighter
`test:integration`).
Stop reading as soon as the terminal `transport_error` event arrives
(cancelling the reader) instead of waiting only for `done`. That event is
always the last one the session queue emits, so nothing is lost, and the
test no longer depends on stream-close timing. The `done` break is kept
for any stream that closes first.
Verified green across two consecutive full `npm run test:coverage` runs
(3094 tests) where it previously hung, plus isolation (~0.5s).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(web): keep server cards a fixed height (#1528)
Two causes made server cards ragged-height:
1. The command/URL box wrapped long content onto multiple lines, growing
the card. Add a `nowrap` Code variant (single line, ellipsis on
overflow) and a `wrap` prop on ContentViewer (default true); ServerCard
passes `wrap={false}`. The full value stays available via the copy
button.
2. At ~1000-1500px the grid packed cards narrow enough that a connected
card's action row (Clone/Edit/Remove + Connection Info/Settings) wrapped
and stacked. Switch the SimpleGrid to container queries with thresholds
that keep every card >= ~500px wide at each column count, so it drops to
fewer/wider columns instead of wrapping.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(web): address review — correct threshold comment, single-line note, title tooltip
- Correct the column-threshold comment in ServerListScreen: the values
(1040/1560) use N gaps, not N−1; restate the formula accurately so the
numbers aren't "fixed" to match a wrong formula later.
- Note in the ContentViewer `wrap` JSDoc that `false` is for single-line
values only (nowrap collapses embedded newlines).
- Add a native `title` tooltip to the non-wrapping Code box so a clipped
command/URL is readable on hover, not only via copy. Covered by tests.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds enterprise-managed authorization (EMA): browser-side IdP login + RFC 8693 token-exchange and JWT-bearer resource-token mint, per-server EMA opt-in in Server Settings, IdP config in Client Settings, and IdP clientSecret keychain storage. Includes the v2/main merge and review follow-ups (400 on malformed client config; normalizeIdpIssuer import consolidation).
* feat(web,core): edit STDIO cwd/env in Server Settings (#1515)
Persist and edit a STDIO server's working directory and environment
variables through the Server Settings modal.
- Add `env` (pair-array, mirrors headers) and `cwd?` to
`InspectorServerSettings`.
- `storedFieldsToInspectorSettings` lifts the stdio config's `env`/`cwd`
into settings so the modal can show them; they stay on `config` for the
transport. Add `envRecordToPairs` / `envPairsToRecord` helpers.
- `env`/`cwd` remain config-owned on disk (the standard mcp.json location):
the `/api/servers` PUT route writes an edited settings.env/cwd back onto
the stored stdio config on a settings-only patch (supporting clear), and a
config-providing PUT keeps config authoritative. `validateSettings`
accepts `env`/`cwd`.
- Server Settings modal: Working Directory field in Options and a dedicated
collapsible Environment Variables section — both STDIO-only.
- Registry server.json import sets no `cwd` (the schema has no
working-directory concept); documented in serverJson.ts.
Tests: serverList converter round-trip, route write-through (set/clear,
config-precedence, non-stdio no-leak, validation), import preservation
(client-config cwd, registry cwd skip), and form/modal component coverage.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(web): preserve config env/cwd when a settings PUT omits them (#1515 review)
Address Claude's review of #1518.
- `/api/servers` PUT now distinguishes an *absent* `env`/`cwd` (preserve the
stored value) from an explicit empty list / blank string (clear). Previously
`validateSettings` coerced a missing `env` to `[]`, so an env-unaware caller
patching only a timeout would silently wipe the server's env/cwd. The
integrated web client always resends the full GET-rehydrated env, so it was
unaffected — this protects the raw HTTP contract.
- `validateSettings` reports `envProvided`/`cwdProvided`; the write-through only
touches `config.env`/`cwd` for fields the caller actually sent.
- On the omit path, rehydrate the untouched env's keychain values onto the
rebuilt entry so the secret reconcile re-persists them rather than sweeping
the blanked-on-disk keys out of the keychain.
- Minor: guard the stdio cast in `mcpConfigToServerEntries` on the transport
type instead of casting unconditionally.
- Test: settings-only PUT omitting env/cwd preserves config keys + cwd + the
keychain secret.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(web,core): import client configs and registry server.json (#1348)
Wire up the two server-import affordances that were previously todoNoop:
- Import config: source picker (Claude Desktop / Cursor / Cline / VS Code)
read on the backend via a new GET /api/import-source, plus a file-upload
fallback. Per-server review with import/skip on new servers and
overwrite/skip/rename on id conflicts, then a per-server outcome summary.
- Import server.json: parse the MCP registry single-server format
(npm/pypi/oci packages + remotes, env vars, package/runtime args, URL
template vars) into a runnable config, with a file picker and debounced
live validation; Add Server is disabled until validation passes.
Core gains an isomorphic import strategy layer (core/mcp/import) with a
strategy registry, client-config + server.json parsers, a strategy-agnostic
merge, and a dependency-injected source resolver. useServers exposes
importSource(). Modal headers are bold and the redundant server.json section
header is folded into the modal title.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(web): collapse the server.json textarea into a File Contents disclosure
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(web): auto-collapse File Contents disclosure after content loads
Once content is pasted or a file is loaded, collapse the File Contents
disclosure after ~1s with Mantine's built-in accordion animation, signaling
the input was accepted and surfacing the validation/env-var sections. The
disclosure becomes controlled by the modal; clearing the textarea re-opens it.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(web): flash File Contents hover highlight before auto-collapse
Before the File Contents disclosure auto-collapses, paint it with its hover
background for 250ms, then run the collapse over a 325ms animation, so the
collapse reads as intentional.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(web): rename "Import server.json" to "Import from registry config"
Rename the Add Servers menu entry and the modal header.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(web): rename "Import config" to "Import client config"
Rename the Add Servers menu entry and the modal header.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(web,core): move import orchestration into core helpers + web hooks
Make the import logic reusable by CLI/TUI and keep the modals display-only:
- core/mcp/import: add parseClientConfig (mcpServers→servers fallback),
resolveServerId, selectServerJsonOption, and buildServerConfigForSelection,
so the full import (parse → select → build → merge) is callable from any
environment.
- core/storage: extract the pure validateStoreId into a Node-free store-id
module (store-io re-exports it) so the isomorphic import layer can validate
ids without pulling Node deps into the browser bundle.
- clients/web/hooks: useServerJsonImport and useImportClientConfig own all the
wizard/parse/debounce/apply state; ServerImportConfigModal and
ServerImportJsonModal are now thin display shells over the dumb panel.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(web): rename "Import client config" to "Import from client config"
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(web): remove the Cancel button from the registry import modal
The modal header X (and Escape) still dismiss it; drop the redundant Cancel
button (and the panel onCancel prop).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(web): hide validation results + name override until content is present
In the registry import modal, the Validation Results section and the Server
Name override field stay hidden until something has been pasted/loaded into the
textarea.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(web): make Validation Results a collapsible disclosure
Wrap the registry import modal's validation results in a Mantine accordion
disclosure (default open), matching the File Contents section.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(web): Server Name disclosure with read-only derived name + override
Make Server Name a collapsible disclosure containing a read-only "From
configuration" field (the id derived from the server.json name) and the
editable Override field.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(web): scroll to and highlight a freshly-added server
After a manual add/clone or an import (client config / registry), the new
server's card scrolls into view and draws an animated border (Mantine Border
Animate); clicking anywhere on the card dismisses it.
- App tracks highlightedServerId; addServerHighlighted sets it (edits and
conflict-overwrites don't). Threaded through InspectorView -> ServerListScreen
-> ServerCard.
- ServerCard scrolls into view on highlight and wraps in <BorderAnimate>;
onClick clears the highlight.
- Pin @gfazioli/mantine-border-animate@2.0.3 (Mantine 8 compatible; 2.0.4+
requires Mantine 9).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(web): highlighted card fills its cell and its controls work on first click
- The BorderAnimate wrapper is display:flex, so the card was shrinking to
content width while the border spanned the cell. Stretch the card with
w=100% (and the wrapper).
- Always render BorderAnimate and toggle show/animate instead of conditionally
wrapping: swapping the element type on clear remounted the card, swallowing
the click that triggered it (e.g. the connect toggle fired clear but not
connect, needing a second click). Verified: one click now connects + clears.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(web,core): address review — OCI env, untrack fixture, rename validation
- Untrack the stray inspector-v1-multi.server.json (accidentally committed) and
gitignore root *.server.json fixtures.
- core/mcp/import/serverJson: forward declared env vars into OCI containers via
'-e KEY' docker args (they previously only set the docker CLI process env);
add a test. Clarify buildServerConfig (destructure env out instead of
spread-then-delete) and document value-vs-default env handling.
- useImportClientConfig: validate 'rename' targets inline (invalid charset /
collision with existing, additions, or other renames) and gate the Import
button on canImport; surface the error on the field.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(web): highlight all servers from a batch import, not just the last
Track highlighted servers as a set of ids instead of one. addServerHighlighted
accumulates each id (a multi-server client-config import highlights them all);
opening an add/import modal starts a fresh batch. Clicking a card clears only
that card's highlight; only the first highlighted card scrolls into view.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(web): catalog vs read-only session launch model + apply --header (#1481, #1483)
Per specification/v2_catalog_launch_config.md, `mcp-inspector --web` now selects
the backend's server-list source and a session-wide writability, instead of
seeding an inert `initialMcpConfig` the UI never read:
- `--catalog <path>` (and `MCP_CATALOG_PATH`) → active WRITABLE catalog
(seed/CRUD/watch), like the default catalog.
- `--config <path>` → READ-ONLY session file: served in the UI but never
written, seeded, or plaintext-secret-migrated — safe to point at a foreign
config (e.g. claude_desktop_config.json).
- ad-hoc `--server-url`/command (+ `--header`) → one server held IN MEMORY
(no file written); `--header` is lifted onto the entry so the seeded
connection actually uses it (closes the #1483 warn-only no-op).
- nothing → default catalog (writable), unchanged.
Backend (`createRemoteApp`): new `writable` + `initialServers` options. GET
/api/servers branches three ways (in-memory / read-only file with no
seed+no migrate / writable catalog); the four mutating routes return 403 when
read-only; `/api/config` carries `writable`; the file watcher is skipped for
in-memory sessions. `--catalog`/`--config` are mutually exclusive and neither
combines with an ad-hoc target or `--header`; `--header` requires an ad-hoc
HTTP/SSE server (clear errors otherwise — no silent no-op).
UI: `useServerListWritable` reads `writable` from /api/config; the Servers
screen hides add/edit/clone/remove/reorder/settings and shows a read-only
banner when false. All new props default to writable=true (back-compatible).
Docs: spec gaps G2/G3/G4 updated; launcher README documents the flags.
Verified end-to-end against the built launcher: --config (writable:false,
file byte-stable, mutations 403), ad-hoc --header (in-memory, header lifted),
and --catalog (writable:true, POST persists).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(web): address review — Mantine <Code> in read-only banner, clarify watcher comment
- Replace raw <code> with Mantine <Code> in the read-only-session Alert
(repo convention: prefer Mantine primitives over raw HTML).
- Add a comment near `watchable` clarifying that a read-only `--config` file
is still watched on purpose (read-only blocks UI writes, not external
observation; `writable` does not gate watching).
Deferred (per reviewer): consolidating useSandboxUrl + useServerListWritable
into one useConfig hook to avoid the duplicate /api/config fetch — optional
follow-up. The redundant ad-hoc header type-guard is kept as defensive.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(web): section-header hover outline + clear buttons out of tab order (#1487)
Two button-behavior adjustments:
1. History section headers (Pinned / History) now use the filter-toggle
style — a thin outline on hover instead of a background fill — by pointing
the `sectionHeader` Group variant at the shared `.filter-toggle` class. The
open section keeps its filled background (passed per-instance via `bg`).
2. Every input clear button added in #1448 is removed from the keyboard tab
order via `tabIndex={-1}` (CloseButton clear buttons app-wide, plus the
Autocomplete clear button through `theme/Autocomplete.ts` clearButtonProps),
so tabbing through a form moves to the next field, not onto the clear button.
The buttons stay mouse-clickable.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(web): apply filter-toggle hover outline to Resources accordion headers (#1487)
The Resources screen `disclosure` accordion controls now share the same
outline-on-hover treatment as the filter toggle buttons and History section
headers: a thin border on hover instead of a background fill, plus a filled
background when the section is open (keyed off `aria-expanded`). The `.filter-
toggle` class is added to the disclosure control slot; the default-variant
accordion (Server Settings) keeps its background-fill hover via a `:not(.filter-
toggle)` guard.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(web): extract ClearButton element, replace 34 inline clear buttons
Addresses the code-review suggestion on PR #1488: the same five-line
`CloseButton` clear-affordance block (aria-label="Clear" + tabIndex={-1} +
onClick) was hand-copied across 17 files. Extract a single
`elements/ClearButton` (Mantine `CloseButton.withProps` with the aria-label and
tabIndex baked in) and replace all 34 call sites, so the clear-button contract
lives in one place. A global CloseButton theme default would be wrong here — it
would also catch tabbable modal-close X buttons and the Select clear buttons —
so a dedicated element is the right altitude.
Net -151 lines. Adds element unit tests + a Storybook play test covering the
aria-label / tabIndex / clickable contract for all sites at once.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(core): trace dropped fetch body update when entry rotates out (#1390)
FetchRequestLogState.onFetchRequestBodyUpdate silently dropped a deferred
response-body update when the matching entry had already been evicted by
log rotation (>= maxFetchRequests newer requests appended between the
append and the fire-and-forget body read). The user saw an empty Response
Body with no way to distinguish "body never came back" from "body dropped
after eviction".
Plumb an optional pino logger into FetchRequestLogState (defaulting to the
silent logger) and emit a logger.debug at the early-return site so the drop
is traceable. The web client wires the existing remote logger from
createWebEnvironment into the state manager.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(core): emit dropped-body trace at warn level so it is observable (#1390)
Review feedback: the web remote logger (createRemoteLogger) defaults to
level 'info' with no override at the environmentFactory call site, so a
logger.debug at the drop site is filtered out by both the pino level gate
and transmit.level — the diagnostic never reaches /api/log and the drop
stays invisible in the shipped web app, defeating the issue's goal.
Promote the trace to logger.warn (>= info, so it transmits by default). A
response body lost to log rotation is unexpected data loss, not routine
diagnostic noise, so warn is the appropriate severity. Update the comment
to explain the level choice and switch the test stub from debug to warn,
extracting a small typed makeWarnLogger helper per review.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(web): configurable Network Log Size per server + body-dropped toast (#1390)
Turn the otherwise-invisible "response body dropped after rotation" case into
an actionable signal:
1. Per-server `maxFetchRequests` setting (default 1000, 0 = unlimited),
editable as "Network Log Size" in the first (Options) accordion section of
the Server Settings form. Plumbed through the full settings pipeline:
InspectorServerSettings + StoredMCPServer types, the serverList disk
converters (read defaults to 1000, write omits the default to keep diffs
minimal — 0 is a meaningful non-default), and the node server's GET-sanitize
+ write-side validateSettings (non-negative number). Applied live to the
active connection on settings-modal close via a new
FetchRequestLogState.setMaxFetchRequests (shrinking trims immediately), and
at connect time for the next session.
2. FetchRequestLogState now emits a `fetchRequestBodyDropped` event, but only
for genuine rotation drops (log at capacity with a finite cap) — not for
benign post-clear stragglers or unlimited logs, which previously also hit
the warn path. App shows a deduped, persistent toast (stable per-server id,
no auto-close) explaining what happened and the likely causes, with a link
that opens this server's settings on the Options section (modal remounts via
key) so the user can raise the limit if it's just a high-traffic server.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(web): don't treat a cleared Network Log Size field as unlimited (#1390)
Review feedback: clearing the NumberInput emits "", and `parseInt("") || 0`
coerced that to 0 (unlimited). Since the value applies live on modal close, a
user who cleared the field to retype and then closed the modal would silently
switch the log to unlimited. Fall back to the current value on an empty/NaN
parse and reserve 0 for an explicit numeric entry. Adds a cleared-input test.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(web): hide resource subscription UI when server lacks subscribe capability (#1478)
Thread a `subscriptionsSupported` flag derived from
`capabilities?.resources?.subscribe === true` from App through
InspectorView → ResourcesScreen to the leaf components. When false:
- ResourcePreviewPanel hides the Subscribe/Unsubscribe button (Refresh
stays).
- ResourceControls omits the Subscriptions accordion section entirely,
dropping it from `allSections` so the expand/collapse-all toggle and
open-section bookkeeping stay correct even if a stale "subscriptions"
entry persists from a prior subscription-capable session.
The flag defaults to true at the leaf components so existing callers,
tests, and stories keep rendering the subscription controls unless a
server is explicitly marked unsupported.
Adds unit tests and Storybook stories covering the hidden and shown
states across all three components.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(web): give the Servers screen scroll the same gutter/idle-hidden treatment (#1474)
The server grid's ScrollArea.Autosize used the default type="hover" with no
offsetScrollbars, so the scrollbar popped in on hover and overlaid the right
edge of the server cards (occluding their action icons / status badges) — the
same problem #1474 fixed for the History/Network/Logging list panels.
Apply the same `type="scroll"` + `offsetScrollbars` treatment, and reuse the
shared `expectScrollbarGutterIdleHidden` story assertion to regression-protect
the gutter + idle-hidden behavior.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(web): stop the InspectorView shell from scrolling as a whole
AppShell.Main carried Mantine's default `padding="md"` on top of the
fixed-header offset, but every screen already sizes itself to
`calc(100vh - header)` and supplies its own `xl` padding (with no
allowance for Main's padding). That extra inset pushed screen content
past the viewport, so the whole InspectorView gained a page-level
scrollbar instead of confining overflow to each screen's inner
ScrollArea.
Set the shell `padding` to 0 (Main now contributes only the header
offset, matching what the screens were built for) and clamp the Main
slot to `height: 100dvh` with `overflow: hidden` in the theme, so the
view can never scroll regardless of content — only the inner ScrollArea
regions do.
Adds an InspectorView story play assertion guarding the invariant
(Main clips + is viewport-clamped) and a ManyServers fixture that
confirms it holds under enough content to overflow, with the inner
server-grid ScrollArea absorbing the overflow.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(web): address review — align viewport units + drop stale subscriptions intent
Responding to the @claude review on #1479:
- Align all screen height calcs from `100vh` to `100dvh` so they match the
Main slot's `100dvh` clamp. On a mobile viewport where `dvh < vh` a `100vh`
child would have exceeded Main's `100dvh` content box and been clipped by
`overflow: hidden`; the units are now in lockstep.
- Restrict `intendedButEmptySections` in ResourceControls to `allSections` so a
stale "subscriptions" entry persisted from a prior subscription-capable
session is dropped from persisted open-sections instead of being perpetually
re-appended once the section is no longer rendered. Adds a regression test.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(web): give History/Network/Logging list scrollbars a gutter, hide when idle (#1474)
The three main-content list panels rendered their ScrollArea.Autosize with
the default type="hover" and no offsetScrollbars, so the scrollbar popped in
on hover and overlaid the right edge of the cards (occluding the action
icons / status badges).
Add `offsetScrollbars` (reserves a 12px gutter so the bar never overlaps the
cards) and `type="scroll"` (the bar shows only during active scrolling, then
fades — no hover-pop) to HistoryListPanel, NetworkStreamPanel, and
LogStreamPanel. Verified in a real browser; adds a story assertion guarding
the gutter + idle-hidden behavior.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(web): assert scrollbar gutter on all three list panels (#1475 review)
Per review, regression-protect the gutter + idle-hidden scrollbar in every
panel, not just History. Extract the assertion into a shared
`expectScrollbarGutterIdleHidden` helper and use it from the History,
Network, and Logging panel stories.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(web): assert a scrollbar exists so idle-hidden check isn't vacuous (#1475 review)
Add `expect(scrollbars.length).toBeGreaterThan(0)` before the loop so the
idle-hidden assertion is meaningful rather than trivially-true if a future
change stops rendering the scrollbar element.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>