3,193 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
8d4363d0ea |
feat(web): expandable resource_link with read-on-demand (#1449) (#1456)
* feat(web): expandable resource_link with read-on-demand (#1449) Replace the placeholder `resource_link` rendering in ContentViewer with a dedicated, Mantine-native `ResourceLink` element that mirrors V1's behavior: - Shows uri (monospace, link-styled), optional name/description, and a mimeType badge. - Expand/collapse affordance (chevron + loading spinner) that is keyboard accessible (native button: role, aria-expanded, aria-label, Enter/Space). - On first expand, reads the linked resource on demand via an injected callback and renders the full read result inline as formatted JSON under a green "Resource:" label, capped to a max height with scroll for large results. The fetched result is cached across collapse/expand. The read handler is threaded as a "dumb component + callback as prop" through ToolResultPanel → ToolsScreen → InspectorView, with App supplying a handler that returns the `ReadResourceResult` from `inspectorClient.readResource`. Also extract the shared `resourceContentsToBlock` helper into `utils/resourceContents.ts` (previously private to ResourcePreviewPanel). Tests: unit coverage for ResourceLink (collapsed/expanded/error/cache) and the ContentViewer wiring, plus Storybook stories with play functions. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(web): scroll large resource_link read results within the card A plain ScrollArea with only `mah` does not scroll when its parent (the content-sized card) has no definite height, so a large read result overflowed the card instead of scrolling. Switch to ScrollArea.Autosize, which grows with content up to the max height and then scrolls. Adds a LargeResult story covering the bounded-scroll behavior. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(web): make ResourceLink a group; extract ResourceLinkInfo element ResourceLink is stateful (expand/loading/cached result) and fetches data via a callback — group-like behavior, and the only `elements/` component using useState. Move it to `groups/` and split out the pure-display metadata as a new `elements/ResourceLinkInfo` element (uri/name/description/mime + an optional action slot). This keeps the layer dependencies clean (no element→group import, which moving ResourceLink alone would have introduced): - ContentViewer (element) renders ResourceLinkInfo (element) statically for `resource_link` blocks and no longer takes an onReadResource prop. - ToolResultPanel (group) renders the interactive ResourceLink (group) for `resource_link` blocks, ContentViewer for the rest. - ResourceLink (group) composes ResourceLinkInfo + ContentViewer. Also reverts the now-single-consumer resourceContents.ts extraction, restoring ResourcePreviewPanel's local toContentBlock. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(web): make ResourceLink read retryable and guard double-fetch Address PR review: a failed read is no longer cached as terminal — only a successful result is cached, so collapsing and re-expanding retries the read after a transient failure. Add a loading guard so toggling while a read is in flight does not fire a redundant second read. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * style(web): prettier formatting for ResourceLink test Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore: remove accidentally-committed build artifacts; gitignore clients/*/build A stray 'git add -A' swept 8 compiled build files into d25c9382. Remove them from tracking and add clients/*/build to .gitignore so client build output is never committed again. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
638f2bfa5b |
Fix resource refresh button not triggering re-fetch (#1148)
* Fix resource refresh button not triggering re-fetch (#1120) The readResource function had an early return when the resource URI was already cached, preventing the Refresh button from re-fetching. Add a force parameter to bypass the cache on refresh, and update the cache-hit path to still sync the displayed content when switching between previously viewed resources. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: don't cache failed resource reads and rename force to bypassCache Failed resource reads were cached in resourceContentMap, causing stale errors to be served on re-click instead of retrying. Errors now only set the display content without polluting the cache. Also renamed the `force` boolean parameter to a `{ bypassCache }` options object for self-documenting call sites. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * test: remove flaky resource-refresh e2e test The test depends on npx downloading @modelcontextprotocol/server-everything at runtime and uses waitForTimeout, making it unreliable in CI. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: invalidate stale cache on refresh failure and rename opts to options Clear cached resource content when a bypassCache refresh fails, so subsequent clicks retry the fetch instead of serving stale data. Rename the inline wrapper parameter from opts to options for consistency. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: surface read errors for resource_link in Tool results Previously, failed resource_link reads left resourceContentMap[uri] empty, so ResourceLinkView rendered an empty expansion with no error feedback when a read failed in the Tools tab. Track errors in a separate resourceErrorMap and thread it through ToolsTab/ToolResults to ResourceLinkView so the user sees the error message. The existing cache-skip behavior is preserved (errors don't pollute the content map, so retries still work). Also adds a ResourcesTab test asserting the Refresh button passes { bypassCache: true } to readResource, to prevent regression of #1120. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: preserve cached content on refresh failure and stop writing error JSON to resourceContent Don't delete resourceContentMap[uri] when a bypassCache refresh fails; a transient network error should not wipe the last-known-good content. To prevent serving stale content after a known error, the cache-hit guard now also skips cache when resourceErrorMap[uri] is set, forcing a re-fetch until a successful read clears the error. Also stop writing JSON.stringify({ error }) into resourceContent in the catch block. sendMCPRequest already sets errors.resources (which the ResourcesTab Alert displays), and the Tools tab shows errors via resourceErrorMap. Writing error JSON into resourceContent was both redundant and could bleed through as JsonView content after callers cleared errors.resources before the next read. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: use presence checks instead of truthiness for resource error state Switch from truthiness checks to presence checks in the resource error/content handling so an empty-string error message is still treated as an error. readResource's cache-hit guard now uses `uri in resourceContentMap` / `!(uri in resourceErrorMap)`, ToolResults passes `resourceError?.[item.uri]` (possibly undefined) through to ResourceLinkView, and ResourceLinkView renders the error branch when `resourceError !== undefined`. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: use hasOwnProperty instead of `in` for resource cache checks The `in` operator also matches inherited properties like `toString` and `constructor`, which would cause false cache hits if a server ever returns a URI with one of those names. Switch the cache-hit guard and the error-map cleanup to Object.prototype.hasOwnProperty.call so only real cached entries match. Object.hasOwn would be cleaner but requires ES2022 lib support which the current tsconfig doesn't include. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: clear resource error on read start, not only on success Previously, resourceErrorMap[uri] was only cleared when a read succeeded. During an in-flight retry (e.g., after clicking Refresh on a previously-failed resource), the stale error would remain in the map and render in ResourceLinkView while the retry was still pending. Clear the error entry after the cache-hit check passes, right before sending the request, so the UI shows the last-known-good content (or a loading state) during the retry instead of the prior error. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: use || instead of ?? for error message fallback The ?? operator only falls back on null/undefined, so an Error with an empty message (new Error("")) would produce an empty string and render a blank error block. Switch to || so empty strings also fall through to String(error), which produces a non-empty representation. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: only sync resourceContent on cache hit from Resources tab The cache-hit path called setResourceContent unconditionally, which meant expanding a resource_link in the Tools tab could overwrite the Resources tab's displayed content without updating selectedResource. Guard the setResourceContent call with a currentTabRef check so only cache hits from the Resources tab update the display. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
a10ced988c |
feat(web): show Apps, Prompts, Resources, and Tasks tabs only when the server exposes the relevant items (#1450) (#1453)
* feat(web): show Apps tab only when server exposes MCP App tools (#1450) Gate the "Apps" tab on appTools.length > 0 so it is hidden when the connected server exposes no MCP App tools. appTools is now computed before availableTabs, which depends on it — the tab appears/disappears live as the tool list changes (list-changed refresh, server switch). The existing activeTab fallback already snaps a user sitting on Apps back to Servers when the tab disappears. When app tools exist but the sandbox is unavailable the tab stays visible so the AppsScreen's "unavailable" message remains reachable. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(web): content-gate the Prompts and Resources tabs too (#1450) Extend the Apps tab gating to Prompts and Resources: each tab is hidden when its list is empty and appears reactively when the list gains an entry (list-changed refresh, server switch). Resources is gated on resources OR templates, since a server may expose only templates. The existing activeTab fallback snaps a user on a now-empty tab back to Servers. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(web): content-gate the Tasks tab too (#1450) Extend the content gating to the Tasks tab: hidden when no tasks exist, appears reactively once a task-augmented tool call creates one and disappears when all are cleared. Consistent with the Apps/Prompts/ Resources gates — the existing activeTab fallback snaps a user on a now- empty tab back to Servers. The "run as task" affordance still lives on the Tools screen (gated by server task support), so discovery is unaffected; advertised capabilities remain visible in Connection Info. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(web): fade content-gated tabs in to draw attention on appearance (#1450) Add a brief 325ms fade-in to navigation tab labels via a new `tabLabel` Text theme variant + `inspector-fade-in` keyframe. SegmentedControl keys segments by value, so when a content-gated tab (Apps/Prompts/Resources/ Tasks) appears after a list change its label mounts fresh and fades in to draw the eye, while existing tabs stay put. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(web): animate tab-bar width when a tab is added or removed (#1450) Wrap the tab SegmentedControl in a clip element (new `tabBar` Group variant) whose width is set to the control's measured border-box size and transitions over 325ms ease-in. The control is sized `max-content` and the clip `overflow: hidden`, so when a content-gated tab appears or disappears the bar grows/shrinks smoothly. Width is measured with useElementSize (ResizeObserver); it falls back to auto width where the observer is unavailable, so the bar still renders at its natural size. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(web): lengthen tab-label fade-in to 450ms (#1450) Bump the `tabLabel` fade from 325ms to 450ms so a newly-appearing tab name fades in more gently. Also formats the previously-unformatted ViewHeader width-clip test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(web): drop tab-label fade-in, keep only the 325ms width animation (#1450) Remove the per-tab fade-in (tabLabel Text variant + inspector-fade-in keyframe + label wrapping) so the tab bar's 325ms ease-in width change is the sole animation when a content-gated tab appears or disappears. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(web): collapse the tab bar to 0 width when it leaves the DOM on disconnect (#1450) Unify the connected/disconnected ViewHeader into a single tree so the tab controls can sit in a keep-alive Mantine Transition. On disconnect the Transition holds the bar mounted for 325ms while the clip's CSS width transition collapses it measured->0, then unmounts. The connected tab list/selection is snapshotted (adjust-state-during-render, no refs) so the bar keeps rendering its tabs as it shrinks. The disconnected header keeps its logo-left / title-center / theme-right layout. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(web): fade the disconnected title in after the tab bar collapses (#1450) On disconnect, hold the "MCP Inspector" title until the tab bar's shrink-to-0 exit finishes (tracked via the keep-alive Transition's onExited), then fade it in over 300ms. Removes the prior overlap where the title appeared beside the still-collapsing bar. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(web): set both disconnect animations to 300ms (#1450) Drop the tab-bar collapse from 325ms to 300ms so it matches the title fade (also 300ms), trimming the overall disconnect sequence. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(web): staggered fade+slide-down crossfade between title and tab bar (#1450) Replace the tab-bar width/size animation with a staggered crossfade. The disconnected "MCP Inspector" title and the connected tab bar share one CSS grid cell, so on connect/disconnect one fades + slides down out while the other fades + slides down in — the incoming one delayed by half the 300ms duration so they're staggered and one replaces the other in place. Both motions travel downward (enter descends from above, exit descends below) via inspector-fade-slide-in/out keyframes, selected per element with a data-anim attribute; Mantine Transition provides keep-alive so the exit plays before unmount. Removes the clip/useElementSize/tabBar-variant size-animation machinery. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(web): pulse a red glow on a tab that newly appears mid-session (#1450) When a content-gated tab is added to the bar while connected, pulse a red text glow around its label in-then-out over 325ms. New tabs are detected by comparing the shown set to the previous render's (adjust-state-during- render), so only mid-session additions glow — not the initial set on connect, nor anything on disconnect. The glow is a CSS keyframe (inspector-tab-glow) applied via a `tabGlow` Text variant + `data-glow` attribute; the red comes from a new --inspector-glow-red token. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(web): start the new-tab glow at full intensity, then fade (#1450) Change the tab glow from an in-then-out pulse to full-on-appear then fade over 325ms (ease-out), so a newly added tab flashes brightest the instant it appears. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(web): keep new-tab label text red through the glow, then revert to theme (#1450) Animate `color` red across the whole glow keyframe so a newly-appeared tab's text stays red for the 325ms; with no forwards fill it snaps back to the label's theme color when the animation ends. The glow text-shadow still fades over the duration. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(web): revert new-tab text color animation, keep glow only (#1450) Drop the `color: red` keyframe stops so the label text keeps its theme color; only the red glow (text-shadow) plays, full intensity on appear then fading over 325ms. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(web): make the new-tab glow ease-in over 325ms (#1450) Change the tab glow timing function from ease-out to ease-in (duration already 325ms). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(web): animate server name and right-side controls on connect/disconnect (#1450) Wrap the server name (left) and the status indicator + Disconnect control (right) in keep-alive Transitions so they fade + slide-down in on connect and out on disconnect, using the same values as the tab bar. Split the animation CSS (.header-anim[data-anim]) from the center-stack grid placement so it's reusable, and broaden the snapshot to retain serverName/ status/latency for rendering during exit. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(web): address review — glow grace window, Select comment, key unification (#1450) - Arm the new-tab glow only after a GLOW_GRACE_MS window post-connect, so asynchronously-resolved lists (prompts/resources/tasks) aren't mistaken for mid-session additions and don't glow on initial load. - Document why the narrow-viewport Select doesn't glow. - Route snapshotKey and the glow key through a shared tabsKey()/TAB_SEP helper (also removes a stray control char from the join separator). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(web): note the slow-server tail of the glow grace window (#1450) Acknowledge in a comment that GLOW_GRACE_MS keys on time-since-connect, not on lists having settled, so a server slower than the window glows on its tail. Per review follow-up. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(web): honor prefers-reduced-motion for the new header animations (#1450) Add a reduced-motion media query that disables the header fade/slide (.header-anim) and the new-tab glow (.tab-glow), leaving elements at their resting state. Per review accessibility suggestion. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
0890c7509a |
feat(web): add Mantine clear buttons to text inputs app-wide (#1333) (#1448)
* feat(web): add Mantine clear buttons to text inputs app-wide (#1333) Add a clear (×) affordance to every user-editable text input across the web client so a populated field can be reset in one click. - Autocomplete: enable `clearable` by default via the theme (`theme/Autocomplete.ts`) — covers the prompt-argument and resource-template variable autocomplete branches. - TextInput / Textarea: Mantine v8 does not expose a public `clearable` prop on these primitives (only the combobox-based components do), so add the clear button manually via `rightSection` + `CloseButton` (aria-label="Clear") shown only when the field has a value, wired to reset the field's controlled value to "". Applied to sidebar search inputs (prompts, tools, apps, tasks, history, resources, network, logs), prompt-argument and resource-template variable inputs, the sampling response/model fields, server config & settings forms, roots table, schema form, experimental features panel, and the server.json import panel. Read-only/display Textareas (OutputValidationModal, UrlElicitationErrorModal) are intentionally left untouched. Tests cover the clear behavior on representative inputs from each category (sidebar search, prompt argument, resource-template variable, sampling response, plus the config/settings forms). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(web): disable ServerConfigModal clear buttons while submitting Address review #2 on #1448: the clear CloseButton sat in `rightSection` with `rightSectionPointerEvents="auto"`, so it stayed clickable even while the field was `disabled={submitting}`, letting a user wipe a field mid-submit. Forward `disabled={submitting}` to each clear button so it locks together with its input. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(web): remove redundant Auto-respond from SamplingRequestPanel The sampling modal pre-loads its draft with the default stub result, and "Send Response" sends that draft — so on an untouched form "Auto-respond" did exactly the same thing as "Send Response", just with a fresh copy of the same defaults. Drop the redundant button and its `onAutoRespond` prop; "Send Response" now covers both the unedited (stub) and edited cases. The InlineSamplingRequest variant keeps its Auto-respond button — there it is the quick path versus "Edit & Send" (which opens this editable modal), not a duplicate of an adjacent send action. Updated PendingClientRequestModal wiring, stories, and tests accordingly. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
62d442b073 |
Revert "First pass at pulling the v1.5 CLI, TUI, and Launcher clients into v2."
This reverts commit
|
||
|
|
467b01edd6 | First pass at pulling the v1.5 CLI, TUI, and Launcher clients into v2. | ||
|
|
d5f12b2a82 |
feat(web): reorderable ServerCards — drag-and-drop + keyboard (#1369) (#1447)
* feat(web): reorderable ServerCards — drag-and-drop + keyboard (#1369) Let users control the order of servers on the Servers screen instead of hand-editing mcp.json. Each ServerCard gets a grip handle (in the header, left of the name) that drives an @dnd-kit sortable; pointer and keyboard sensors both work, with live-region announcements for screen readers. - Backend: PUT /api/servers/order rewrites mcp.json in the supplied order inside the write lock, reusing the atomic-write + watcher-notify path. Registered before /:id so "order" isn't captured as a server id. Rejects with 409 when the id set no longer matches disk and 400 on duplicate / malformed bodies, so a reorder racing an external edit can't drop or duplicate an entry. - Hook: useServers.reorderServers(orderedIds) — optimistic local reorder, PUT, refresh-to-disk-truth on failure. - UI: SortableServerCard wrapper owns the dnd-kit concerns; ServerListScreen wraps the grid in DndContext + SortableContext (rect strategy) with pointer + keyboard sensors and announcements. Reorder math and a11y copy extracted to reorderIds.ts / serverReorder.ts for direct unit testing. Renders plain cards (no grip) when no onReorder is wired. - Wired onServerReorder through InspectorView -> App.tsx. Tests: pure-helper units, hook (persist / optimistic / 409 revert), 9 backend integration tests (incl. id-set-mismatch rejection and the route-registration guard), ServerCard/ServerListScreen units, and a real-browser Storybook keyboard-reorder play (the dnd-kit keyboard sensor needs layout rects that happy-dom doesn't provide). Adds @dnd-kit/core, @dnd-kit/sortable, @dnd-kit/utilities. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(web): notify on reorder failure; skip reorder glue when not reorderable Address PR #1447 review: - App.tsx: catch reorderServers rejection (409 from a racing external edit, or a network error) and show a red toast, matching the error-handling pattern used by every other mutation — previously the rejection was discarded with `void` and the drag silently bounced back. - ServerListScreen: only build handleDragEnd / announcements when `reorderable`, so the four-closure announcements object isn't allocated every render when no onReorder is wired. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
721dae08c6 |
feat: apply auto-refresh setting live + diff-aware list-changed indicator (#1444) (#1445)
* feat: apply auto-refresh setting live + diff-aware list-changed indicator (#1444) Two follow-ups to #1402. 1. Live setting application — InspectorClient gains setServerSettings() (on the protocol + fake); App pushes edited settings onto the live client when the Server Settings modal closes for the connected server, so toggling auto-refresh-on-list-changed takes effect without a reconnect (the managed state reads getServerSettings() at notification time). 2. Diff-aware indicator — in flag-only mode, on */list_changed the managed Tools/Prompts/Resources state now fetches the new list and compares it to the current one, lighting the indicator only when it actually differs. The displayed list stays put until the user pulls via Refresh (pull-on-demand preserved). Servers like everything re-send identical lists on list_changed; this suppresses the false-positive indicator. Refactors each manager's refresh into fetch + apply helpers shared with the peek path. Trade-off: item 2 reintroduces a fetch on */list_changed (to compare), but unlike the pre-#1402 behavior it never replaces the displayed list under the user — only the indicator reflects a real change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(core): make the diff-aware peek clear the indicator on revert (#1444 review) peekForChange now sets the list-changed flag to whether the server list differs from the displayed one (setListChanged(differs)) instead of only ever setting it true. So a later notification that reverts the server back to the displayed list clears the indicator — nothing left to pull. Adds a clear-on-revert regression test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(core): consolidate managed-list state managers onto a shared base (#1444) The four per-primitive list managers (tools, prompts, resources, resource templates) were near-identical copies of the same fetch/apply/peek/connect/ disconnect/list_changed machinery, a standing drift risk. Extract a generic ManagedListState<T, M> base parameterized by a config (change event, list_changed notification, capability key, page fetcher, and whether the list drives an indicator). Each manager is now a ~40-line subclass: its config plus a typed getter alias. Public contracts are unchanged — same per-manager *Change / listChangedChange events and get*/refresh/getListChanged/clearListChanged methods — so the hooks and all existing tests pass untouched. Templates opt out of the indicator (supportsIndicator: false): a list_changed auto-refreshes only when the server opts in, else does nothing. Net -245 lines. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * perf(core): coalesce bursts of list_changed peeks (#1444 review) A rapid burst of */list_changed notifications fired one concurrent paginated fetch per notification. Guard peekForChange with an in-flight flag: while a peek is fetching, a new notification queues a single re-run instead of launching a concurrent fetch, and the running peek loops once more on completion to capture the latest server state. Collapses a burst into sequential peeks. Adds a coalescing test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(core): debounce list_changed bursts into a single fetch (#1444) The in-flight guard only coalesced fetches that actually overlapped — when a server (e.g. everything) emits a burst of */list_changed spaced just far enough apart that each list call returns before the next notification, we still fired one list call per notification. Add a real debounce in the ManagedListState base: a notification (re)starts a timer and the refresh/peek runs once the burst settles, collapsing N notifications into one fetch. The in-flight coalescing guard stays as a second line of defense for a post-debounce notification landing during a slow paginated fetch. The delay is injectable (DEFAULT_LIST_CHANGED_DEBOUNCE_MS = 250; subclasses take an optional constructor arg) so tests run with 0 for fast, determinist ic behavior. Timer is cleared on disconnect, unsubscribe, and destroy. Adds debounce + in-flight regression tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(core): extend the in-flight guard to the auto-refresh path (#1444 review) The overlap guard was only on the peek path; a burst-during-a-slow auto- refresh could let a slower older fetch clobber a newer list (applyItems is last-write-wins). Lift the guard from peekForChange up to runListChanged so it wraps both the auto-refresh and peek actions: while one is fetching, a new post-debounce notification queues a single re-run. Adds an auto-refresh coalescing test alongside the peek one. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(core): light the indicator without fetching when auto-refresh is off (#1444) Per maintainer feedback: with auto-refresh off, a list_changed should not issue any list call. The diff-aware peek (fetch-to-compare) meant one tools/list went out per settled burst even with auto-refresh off, which the maintainer doesn't want. Revert flag-only mode to lighting the indicator blindly on a (debounced) list_changed — no network — and the user pulls the new list via Refresh. Trade-off (accepted): the indicator lights even when the server re-sent an identical list (the everything-server case), since we no longer fetch to compare. Keeps the debounce (one indicator light per burst), the auto-refresh path + its in-flight guard, the live setServerSettings, and the ManagedListState consolidation. Removes peekForChange and its tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(core): scrub stale peek docstrings after the blind-indicator revert (#1445 review) The diff-aware revert removed peekForChange's body but left its docstring, which orphaned onto destroy() and described fetch-to-compare/clear-on-revert behavior that no longer exists. Replace it with a one-line destroy() doc, and update clearListChanged's doc (peek -> blind light). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(core): finish scrubbing stale peek references after the revert (#1445 review) Round-7's scrub missed four more peek/fetch-to-compare mentions in managedListState.ts (supportsIndicator, fetchAll, the debounce-default and onListChanged comments) plus six 'async peek' comments in the state tests. Update them all to the blind-indicator semantics. Doc/comment-only; no behavior change. grep -i peek over core/mcp/state now returns nothing. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
b0a024269e |
feat(web): list-changed handling — indicator, pull-on-demand default, and per-server auto-refresh opt-in (#1402) (#1443)
* feat(web): wire the listChanged indicator across all screens (#1402) The per-screen list-changed indicator was hardcoded false in InspectorView for Tools, Apps, Prompts, and Resources, so it could never light up. Source it from the managed-state layer instead. Each ManagedXState now tracks a listChanged flag: set true when its */list_changed notification arrives (the existing auto-refresh leaves it set), exposed via getListChanged()/a listChangedChange event, and cleared by clearListChanged() or on disconnect. The useManagedX hooks surface the flag and clear it on a user-initiated refresh (the indicator's Refresh button = acknowledge). App threads the three flags to InspectorView, which maps them to the screens — Apps shares the tools flag since apps are a filtered view of tools. Tests cover set/clear/disconnect at the state-manager layer, the hook subscription + refresh-clears behavior, and the InspectorView routing (including Apps sharing the tools flag and no cross-wiring between screens). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(core): close clear-after-await race in managed-list refresh (#1402 review) The useManagedX refresh() callbacks cleared the listChanged flag AFTER awaiting the fetch. If a */list_changed arrived mid-fetch, the state re-set the flag (and auto-refreshed), then the trailing clear wiped that genuinely-new signal. Clear BEFORE the await instead: the user's refresh acknowledges only the change in hand, and a mid-fetch change re-sets the flag and survives. Adds a race regression test on the tools hook. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(web): drop duplicate Refresh on the Apps screen (#1402) AppControls uniquely had a standalone toolbar Refresh button AND the ListChangedIndicator on a separate row, so once the indicator could actually appear (this PR), the Apps screen showed two Refresh buttons. Match the other three controls (Tools/Prompts/Resources): the indicator is the single header-right refresh affordance. Updates AppControls and AppsScreen tests to assert exactly one Refresh (and none when the list hasn't changed). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(core): stop auto-invoking */list on list_changed; user pulls via Refresh (#1402) With auto-refresh on, the list was already current by the time the list-changed indicator appeared, making its Refresh button moot. Now the managed Tools/Prompts/Resources state managers only flag the change on */list_changed (they no longer auto-refresh); the list stays as-is until the user clicks Refresh, which fetches the new list and clears the indicator. The connect-time initial load is unchanged. Resource templates (no own indicator, part of the Resources screen) also stop auto-refreshing; onRefreshResources now refreshes both resources and templates, since a single resources/list_changed covers both. Tasks are left auto-refreshing — they have no list-changed indicator and need live status updates. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(core): correct ManagedResourceTemplatesState header comments (#1402 review) The class no longer subscribes to resourceTemplatesListChanged; update the stale file/class docstrings that still claimed it syncs on that notification to reflect connect + statusChange only. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(web): add per-server 'Auto Refresh on List Changed Notifications' option (#1402) New Options section in ServerSettingsForm with a single checkbox. When checked, the managed Tools/Prompts/Resources (and resource templates) state auto-refreshes on */list_changed notifications; when unchecked (default) the notification only lights the list-changed indicator and the user pulls via Refresh. Plumbing: new optional autoRefreshOnListChanged on InspectorServerSettings and StoredMCPServer, round-tripped through serverList (lifted with a false default, persisted only when true, added to the exhaustive field-key map); surfaced from InspectorClient via a new getServerSettings() on the protocol; the state managers read it live at notification time to decide auto-refresh vs flag-only. Resource templates re-subscribe to their list_changed but only auto-refresh when the option is on (they have no indicator of their own). Tests cover the round-trip (true persists, false omitted), the form section (render/checked/toggle), and each state manager's on/off branch. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(server): persist autoRefreshOnListChanged through validateSettings (#1402) The PUT /api/servers write path runs settings through validateSettings, which rebuilds the value from explicitly-named fields and silently drops unknown keys — so autoRefreshOnListChanged never reached disk (the serverList round-trip test passed because it calls inspectorSettingsToStoredFields directly, bypassing this path). Validate it as an optional boolean and include it in the built value; false is omitted on disk by inspectorSettingsToStoredFields as before. Adds integration tests over the real PUT path: true persists, false is omitted, and a non-boolean is rejected (400). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(web): default the Server Settings modal to the Options section (#1402) Open 'Options' instead of 'Custom Headers' when the Server Settings modal first opens, so the new Auto Refresh toggle is the first thing surfaced. Updates the modal default-expanded section, the modal test's initial-state assertions, the three header tests (now expand 'Custom Headers' first since it's no longer open by default), and the story defaults. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore: drop accidentally-committed Inspector history dump; gitignore the class 83076441 swept an exported Inspector message-history JSON (manual-testing artifact at the repo root) into the tree via git add -A. Untrack it and extend the existing repo-root debug-artifact .gitignore block (alongside /*.png) to cover /inspector-history-*.json so these can't be re-added. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
9b558afee1 |
feat(web): display negotiated MCP protocol version on ServerCard (#1324) (#1442)
* feat(web): show negotiated MCP protocol version on ServerCard (#1324) Add optional protocolVersion to ConnectionState and render it as a dimmed "MCP <version>" label at the end of the transport-badge/ description row on ServerCard, shown only when connected. The value is plumbed through useInspectorClient as part of #1324. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(core,web): surface negotiated protocolVersion end-to-end (#1324) Plumb the MCP protocol version negotiated during initialize from the InspectorClient through to the ServerCard so the active connected card displays "MCP <version>". The SDK Client doesn't expose protocolVersion; it stamps it onto the transport via setProtocolVersion. MessageTrackingTransport now captures it (and forwards to the base transport when present), so it works for stdio too — whose base transport has no setProtocolVersion. - core: MessageTrackingTransport captures + exposes protocolVersion; InspectorClient stores it, exposes getProtocolVersion(), dispatches a protocolVersionChange event, and clears it on disconnect. - core: useInspectorClient subscribes and returns protocolVersion. - web: App reads it from the hook (drops the hard-coded "2025-06-18") and InspectorView splices it onto the active server's connection. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(web): decouple initializeResult from protocolVersion (#1324 review) Gate the initializeResult memo on serverInfo only, not protocolVersion. initializeResult also drives the connected ViewHeader and Connection Info modal, so requiring the version risked hiding the whole connected surface in the edge case where it's absent. protocolVersion now flows through as the optional field it already is everywhere downstream ('' reads as unknown; the ServerCard label and modal value both tolerate it). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(web): drop redundant connected-check in InspectorView splice (#1324 review) initializeResult is derived from connectionStatus in App's memo, so a truthy initializeResult.protocolVersion already implies a live connection — the extra connectionStatus === 'connected' guard in the splice was redundant. ServerCard keeps its own status guard (dumb- component robustness). No behavior change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
121f875a0e |
feat(web): History — Pin & Replay, plus two history-status fixes (#1438) (#1441)
* fix(web): don't show a permanent PENDING badge on history notifications (#1439) `extractStatus` derived the history status badge purely from whether a `response` was attached, so a fire-and-forget notification (no id, no response ever) — and any unmatched standalone response — was labelled "Pending" forever. The pending → OK/Error lifecycle only applies to request entries (messageLogState attaches the response by JSON-RPC id), so scope the status to `direction === "request"` and render no request-style badge for notifications/responses. The method badge already labels them. Part of the History-screen work on #1438. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(core): track outgoing responses so server→client requests resolve in history (#1440) MessageTrackingTransport.send only tracked outgoing requests, so the client's response to a server→client request (roots/list, sampling, elicitation) was never recorded. The inbound request entry never got its response folded in, leaving it stuck "Pending" with no response body. Make send symmetric with onmessage: track outgoing responses (by id + result/error) as well as outgoing requests. The response now correlates to its request entry, which resolves to OK/Error with the body. Outgoing notifications remain untracked (separate gap). Part of the History-screen work on #1438. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(web): wire up History Pin and Replay (#1438) Both HistoryEntry actions were rendered but stubbed to todoNoop in App. - Pin: App now owns a session-scoped Set<string> of pinned entry ids, toggled by onTogglePinHistory and passed down as pinnedHistoryIds. HistoryListPanel already sorts pinned entries to the top. The set resets with the rest of the per-screen state on disconnect/server-switch. - Replay: onReplayHistory re-issues the entry's original request by method (tools/call → callTool, prompts/get → getPrompt, resources/read → readResource). The call flows through InspectorClient → tracked transport → message log, so the fresh request+response surface as a new History entry (history-local) without touching the Tools/Prompts/ Resources panels. Unsupported methods / a removed tool surface a toast. Tests: pin toggle propagation, replay dispatch for each supported method, unsupported-method and missing-tool toasts. Closes #1438. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(web): make history Replay support list/discovery requests Replaying a tools/list entry reported "Replay isn't supported". Extend replayHistoryRequest beyond the call-style requests to the read/discovery methods, re-issuing each via its InspectorClient method (preserving the pagination cursor): - tools/list → listTools, prompts/list → listPrompts, resources/list → listResources, resources/templates/list → listResourceTemplates, ping → ping. Tests: tools/list replay (cursor preserved) and an unsupported-method toast (logging/setLevel). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(web): replay tasks/list; reorder + conditionally show History buttons - Add tasks/list to the replayable set (re-issued via listRequestorTasks), completing the client-issued */list methods. - Reverse the HistoryEntry actions to Pin then Replay, and hide Replay for entries whose method can't be replayed. - Introduce a single source of truth — REPLAYABLE_HISTORY_METHODS / isReplayableHistoryMethod in historyUtils — used by HistoryEntry to show/hide the button and by App's replayHistoryRequest to gate dispatch, so the two can't drift. Tests: tasks/list replay, Replay hidden for a non-replayable method, and Pin-before-Replay ordering. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(web): add client↔server direction badge to History and Network (#1437) A dual-state badge at the top-left of each entry shows which way it traveled: "client → server" (green) for inspector-originated, "client ← server" (yellow) for server-originated. - New shared element MessageDirectionBadge (Elements). - History: MessageEntry gains an `origin` ("client" | "server") set at tracking time — MessageTrackingTransport tags `send` as client and `onmessage` as server, threaded through the track callbacks. HistoryEntry maps origin → badge. (direction stays the request/response/notification message-type used for response correlation; origin is the flow.) - Network: fetches are always inspector-originated, so NetworkEntry renders the badge as outgoing. Tests + stories for the badge; HistoryEntry direction tests; transport test asserts the origin argument. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * style(web): use purple (not yellow) for the client ← server direction badge Yellow + green read as caution/ok status; purple vs green reads as direction. Incoming (server → client) is now violet; outgoing stays green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(web): make History Pinned/History sections collapsible toggles The "Pinned Requests (N)" and "History (N)" section headers are now clickable toggle buttons that expand/collapse their entries — styled like the LogControls level toggles (UnstyledButton listItem variant with the active background) and wrapping the entries in a Collapse. Both default open and collapse independently. Tests assert the per-section aria-expanded toggling and independence. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(web): replace Expand/Collapse text button with an icon toggle History, Network, and Task cards now use a shared ExpandToggle ActionIcon instead of the "Expand"/"Collapse" text button: LuArrowDownToLine when collapsed (expand), LuArrowUpFromLine when expanded (collapse). The aria-label stays "Expand"/"Collapse" so the control reads the same. - New shared element ExpandToggle (+ test + stories). - HistoryEntry: actions split into a left group (Pin/Replay) with the toggle pushed right via space-between. - NetworkEntry / TaskCard: drop their now-unused SubtleButton const. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(web): pin icon toggle on HistoryEntry, moved to the right Replace the Pin/Unpin text button with a shared PinToggle ActionIcon (TiPinOutline when unpinned, TiPin when pinned; aria-label stays Pin/Unpin). Move it to the right action group, just left of the expand/collapse toggle — Replay stays on the left. - New shared element PinToggle (+ test + stories). - HistoryEntry action row: Replay left; Pin + expand toggle right. - Drop the now-unused formatPinLabel helper. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(web): per-section Clear/Export on the Pinned and History headers Each section header now shows Clear and Export links on the right, but only when both sections are present (a single section is already covered by the panel-level Clear/Export). They act on just that section's entries: - Clear: removes that section's entries by pin membership (clearing the Pinned section also drops the now-stale pinned id set). - Export: downloads just that section's entries (inspector-history-pinned/-unpinned-...json). Threaded onClearSection/onExportSection App → InspectorView → HistoryScreen → HistoryListPanel; App computes each section from messages + pinned ids. Extracted a CollapsibleSection subcomponent and a SectionActions links group. Extended ExportKind with history-pinned/history-unpinned. Tests cover both-sections visibility gating and per-section dispatch. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(web): Filter by Message Direction on the History controls Add a "Filter by Message Direction" section to HistoryControls, modeled on LogControls' "Filter by Level": a Select/Deselect All control plus two listItem toggles — "client → server" and "client ← server" — that show or hide entries by origin. - HistoryUiState gains visibleDirections (Record<MessageOrigin, boolean>), defaulting to both visible; persists with the other per-screen state. - HistoryScreen wires the toggles (single + all) through onUiChange and passes the filter to HistoryListPanel, where matchesFilters drops entries whose direction is off (entries without a recorded origin are never filtered out). Tests cover the controls (render, single toggle, select/deselect all), the HistoryScreen onUiChange wiring, and the list-level direction filtering. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * style(web): section Clear/Export as subtle buttons, not link anchors Match the Select/Deselect All control in HistoryControls: the per-section Clear/Export on the History/Pinned headers now use a subtle Button (variant subtle, size xs) instead of an Anchor link. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * style(web): color the direction filter labels to match the badge The "client → server" / "client ← server" toggles in HistoryControls now color their labels green / violet, matching the MessageDirectionBadge. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(web): track outgoing client notifications in history too Review follow-up (#1441): MessageTrackingTransport.send now also tracks the client's own notifications (initialized, progress, roots/list_changed) as origin "client", making send fully symmetric with onmessage. Previously only id-bearing outgoing messages (requests, responses) were tracked, so client notifications never reached History while incoming server notifications did. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(web): Filter by Message Direction on the Network screen Add the same "Filter by Message Direction" section to NetworkControls as on HistoryControls. Extracted the shared section into a MessageDirectionFilter group component (used by both HistoryControls and NetworkControls) so they stay identical — a Select/Deselect All control plus green/violet client → server / client ← server toggles. - NetworkUiState gains visibleDirections (default both visible), persisted with the rest of the per-screen state; NetworkScreen wires the toggles and NetworkStreamPanel filters on them. - Network fetches are always inspector-originated, so all entries are client → server: the client toggle gates them all and the server toggle matches nothing (documented in NetworkStreamPanel). The section mirrors History for parity. Tests cover the shared filter, both controls' direction sections, the Network list filtering, and the NetworkScreen wiring; ambiguous "Deselect All" selectors (now two sections) disambiguated by order. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * revert(web): drop message direction from the Network tab Network traffic is always client → server — even SSE is a client-initiated stream tagged outgoing — so a direction badge/filter there is meaningless. Remove the MessageDirectionBadge from NetworkEntry and the Filter by Message Direction section (and its NetworkUiState.visibleDirections wiring) from the Network screen. History keeps its direction badge + filter; the shared MessageDirectionBadge and MessageDirectionFilter components stay (still used by History). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(web): a lone History section isn't a collapsible accordion When only one of Pinned / History is present, its header is now a plain title and its entries always show — instead of a `listItem` accordion toggle that could hide them based on a stale collapsed state from when both sections were on screen. The accordion (and per-section Clear/Export) only applies when both sections are present. Example fixed: collapse History while pins exist, then unpin everything — History becomes the sole section and shows its entries under a plain header rather than staying collapsed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * style(web): put section Clear/Export on the header pleat itself The per-section Clear/Export sat to the right of the collapsible header bar, on the panel background. Make the whole header row the "pleat": a new Group `sectionHeader` theme variant (rounded + hover, active bg passed per instance) wraps a flex-1 toggle button (the title) on the left and the Clear/Export buttons on the right — so the actions live on the pleat. The toggle is a separate button since the action buttons can't nest inside it. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * style(web): entry expand toggle uses the same icons as ListToggle Swap ExpandToggle (History / Network / Task cards) from the LuArrow icons to RiExpandVerticalLine / RiCollapseVerticalLine — the same expand/collapse icons the list-level ListToggle uses. Same ActionIcon size, so it matches the other small buttons on the cards; aria-labels unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(web): panel Clear keeps pinned entries (history-only) Review follow-up (#1441): the panel Clear button is gated on the unpinned count (implying history-only), but onClearHistory wiped everything (pinned included) and left pinnedHistoryIds stale. Make Clear history-only — clearMessages((m) => !pinnedHistoryIds.has(m.id)) — so pinning protects an entry, matching the gating and the per-section model; the pin set stays valid since its entries survive. Also drop the stale "and Network" mention from the MessageDirectionFilter docstring (Network direction filter was reverted). Test: panel Clear's predicate keeps pinned ids and removes unpinned. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
a523a0f15a |
fix: call tasks/result when task status is input_required (#1174)
Per the MCP spec (2025-11-25), when a client observes input_required status during task polling it must call tasks/result to give the server a chance to deliver queued elicitation/sampling requests. Previously the polling loop only called tasks/result after completed status, leaving the server unable to trigger elicitation. After elicitation is handled the task transitions back to working so taskCompleted is not set — polling continues until a terminal status is reached. |
||
|
|
5cabc1ecd7 |
feat(web): URL elicitation — explicit completion step + error-path (-32042) handling (#1415) (#1436)
* feat(web): two-step URL elicitation completion (#1415) Replace accept-on-open with an explicit completion step for URL elicitations. Opening the URL now only transitions the panel to its waiting state; the elicitation resolves with `accept` only when the user clicks the revealed "I've completed it" action, or with `cancel`. - ElicitationUrlPanel: add `onComplete`, reveal the completion button and relabel open→"Reopen in Browser" while `isWaiting`. - PendingClientRequestModal: wire local `isWaiting`; open no longer sends a response. Removed the accept-on-open comment that pointed here. - Tests cover open→waiting→complete and open→cancel, and assert opening alone does not resolve the elicitation. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(web): handle URL-elicitation-required (-32042) error path Implements the spec's "URL mode with elicitation required error" flow (2025-11-25). When a tools/call returns a `-32042` UrlElicitationRequired error, the client now surfaces the required URL elicitation(s) and retries the call once completed, instead of showing a bare error. Core (InspectorClient.callTool): - Detect `-32042` (typed UrlElicitationRequiredError or generic McpError) via the new `getUrlElicitationsFromError` helper. - Surface each carried elicitation in order through the existing pending- elicitation queue / modal (reusing the two-step "Open → I've completed it" UI), then re-issue the original call once all are accepted. A decline/cancel aborts with a clear message; a bounded retry count guards a server that keeps returning the error. - `ElicitationCreateMessage.completeIfPending()` lets the optional `notifications/elicitation/complete` auto-advance/auto-accept an open URL elicitation; the completion-notification handler now resolves (was: only removed) the pending entry. - Extracted `attemptToolCall` / `dispatchFailedToolCall` from the old callTool body to support the retry loop without duplicating bookkeeping. Web (App): - A non-spec `-32042` with no elicitations (nothing to open) shows a dedicated yellow toast linking to a new UrlElicitationErrorModal with the raw error body, mirroring the output-schema-validation toast. Tests: helper unit tests, completeIfPending unit tests, callTool error-path retry/cancel/no-list/ordinary-error tests (injected fake client), the new modal (test + stories), and an App toast test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(web): abort URL-elicitation error path on a repeated URL Guard the -32042 retry loop: if the server's retry error re-requests a URL the user already completed earlier in the same call, completing it again can't make progress, so callTool aborts with a new typed UrlElicitationLoopError instead of re-prompting (which would loop until the retry cap). The web layer detects it and shows a "URL elicitation loop" toast naming the repeated URL rather than a generic failure. Tests: core abort-on-repeat test (URL presented once, two attempts, then loop error) and an App loop-toast test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(web): let Copy-URL reach the URL-elicitation completion step Address review of #1436: the "I've completed it" action was gated behind clicking "Open in Browser" (only that set isWaiting), so a user who used Copy URL to finish the flow externally could only Cancel. Copying the URL now also moves the panel into the waiting state, exposing the completion step. Also trims the now-redundant second sentence of the waiting hint. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(core): address review of error-path URL elicitation Review follow-ups on #1436 (all non-blocking): - Remove the leftover bare block scope in attemptToolCall (extraction artifact); body de-indented one level. - Log a warning when callTool gives up after the URL-elicitation retry cap is hit, so a server that keeps demanding new elicitations is diagnosable rather than looking like an ordinary failure. - Assert the dispatch-once invariant on the loop-abort and cancel-abort paths (exactly one failed toolCallResultChange event). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * style(core): fix attemptToolCall body indentation Run prettier over the extracted attemptToolCall method body, which was left over-indented by 2 spaces after the bare-block removal in 0754b43d. core/ is outside clients/web's format:check scope, so npm run validate did not catch it. Whitespace-only; the pre-existing line 658 wrap (also outside the format scope) is left as-is, out of scope for this PR. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(core): settle pending elicitations on disconnect to avoid callTool hang Address review of #1436: an error-path `awaitUrlElicitation` promise only settled via respond, but `disconnect()` cleared `pendingElicitations` wholesale — so a disconnect mid-elicitation (server crash, manual disconnect, transport close) left `callTool` (and the UI's in-flight toolCallState) hanging forever. - Add `ElicitationCreateMessage.cancel()`: settles a pending elicitation as `{ action: "cancel" }` without calling `onRemove` (the queue is cleared by the caller, so it must not splice mid-iteration). - `disconnect()` now cancels each pending elicitation before clearing the array, and dispatches `pendingElicitationsChange` (it already did so for samples) so the modal closes on a transport-close teardown too. Tests: disconnect-mid-elicitation settles callTool (no hang); cancel() unit tests (resolves-without-remove, no-op once responded). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
49d6be6987 |
feat(web): add per-server Roots config + client roots capability (#1425) (#1431)
* feat(web): add per-server Roots config + client roots capability (#1425) Add MCP roots support to the v2 web Inspector as a per-server config section in the server settings form (no separate Roots tab/screen). - Persistence: add `roots: Root[]` to `InspectorServerSettings` and `StoredMCPServer`; thread through serverList converters (lift on read, drop empty-uri rows / blank names on write, omit when empty) and the remote server's read-side drop guard + `validateSettings`. - Capability: `setupClientForServer` always advertises the roots capability (`roots: cleanRoots(savedSettings?.roots ?? [])`) so the server can issue `roots/list` and receive `roots/list_changed`. - Live apply: edits to a connected server's roots are pushed via `inspectorClient.setRoots(...)` once, on settings-dialog close — diffed against the client's current roots — rather than per keystroke, to avoid flooding the server with `roots/list_changed` / `roots/list`. - Form: new Roots accordion section with a required URI and an optional name per row (unlike v1, which was URI-only), + Add Root, and remove. Tests cover the persistence round-trip (serverList + servers-route), the form/modal section, and reuse the existing InspectorClient roots/`setRoots` integration coverage. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(roots): address review — dedup guards/cleanRoots, preserve extra fields, test live-apply Responding to the PR #1431 code review: - Dedup `isRootArray`: drop the copy inside `validateSettings` and reuse the module-scope guard the read path uses, so the two can't drift (core/mcp/remote/node/server.ts). - Dedup the clean-roots normalizer: hoist a single exported `cleanRoots` into core/mcp/serverList.ts and reuse it from both `inspectorSettingsToStoredFields` and App.tsx (drops the local copy). - Preserve non-form root fields (e.g. `_meta`): `cleanRoots` now spreads the carried-through fields and only normalizes uri/name, and the modal's `handleRootChange` spreads the existing root on edit — so a hand-edited root's `_meta` survives load + persist + edit instead of being silently dropped. - Test the App live-apply-on-close path: new App.test.tsx cases assert setRoots is called once with cleaned roots when changed on the active server, and not called when the cleaned roots already match what the client advertises. Adds a cleanRoots unit test (blank drop + _meta preserve). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
ed6eda7492 |
feat(web): wire up MCP task support — Tasks screen, Run-as-task, status toasts (#1422) (#1424)
Wires the ported Tasks screen + core task subsystem through App.tsx: sticky Clear Completed, cancel/refresh error toasts, core-side per-task progress correlation, Run-as-task on the Tools screen (gated on server support), per-server Task TTL, and live task-status toasts. |
||
|
|
0ba1b8d1d8 |
chore: bump version to 0.22.0 (#1428)
Some checks failed
CLI Tests / test (push) Has been cancelled
* Initial plan * chore: bump version to 0.22.0 --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> |
||
|
|
5c782ada7c |
feat: add URL-mode elicitation support (#1423)
* feat: add URL-mode elicitation support
The MCP spec (2025-11-25) added URL-mode elicitation alongside form mode,
allowing servers to direct users to external URLs for out-of-band
interactions (auth flows, payments, sensitive data). This implements
client-side support for rendering and handling URL-mode elicitation
requests, and announces the capability during initialization.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: add consent dialog for URL elicitation and separate Accept button
Replace the clickable URL link with a consent dialog that displays the
URL as plain text when "Open URL" is clicked, letting the user confirm
before navigating. Add a separate "Accept" button that resolves the
elicitation without the content field, per the MCP spec.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: preserve structured McpError data in request history
When a tool call returns an MCP error (e.g. -32042 URLElicitationRequired),
the catch block was only capturing error.message (a string), discarding the
structured error.code and error.data fields.
Now McpError instances push the full JSON-RPC error structure to history:
{ error: { code, message, data } }
This means the Response panel will show the complete error including
data.elicitations for -32042, rather than just a message string.
* Revert "fix: preserve structured McpError data in request history"
This reverts commit 86f6d093f6596d6a9a52b85df24b66fa2e354d07.
* fix: enforce HTTPS-only for URL elicitation per SEP-1036
Address Claude review on #1423:
- Validate the server-supplied URL before opening it; refuse non-https
schemes (http/file/javascript/data) and malformed URLs, disabling the
"Open URL" button and showing an error state instead of calling
window.open (SEP-1036 URL Security Requirements, a spec MUST).
- Display the parsed destination host prominently in the consent dialog
to surface look-alike/embedded-credential URLs.
- Document that elicitation/complete is surfaced via the notification
panel and resolved manually (no auto-correlation), a conscious choice
per the spec's MAY/SHOULD.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: evalstate <1936278+evalstate@users.noreply.github.com>
|
||
|
|
8923a9bbf9 |
feat(web): tool progress toasts + persist all per-screen state across navigation (#1414, #1417) (#1420)
* feat(web): tool progress toasts + persist tool selection/result across navigation (#1414) Surface incoming notifications/progress as toasts so the user can watch a long-running tool's progress without leaving the tool view — the v2 replacement for v1's always-visible "Server Notifications" shelf. Toasts are keyed by progress stream and updated (not stacked) per tick so a chatty server updates one toast rather than flooding the corner. Progress notifications still land in the History tab via the existing transport-level tracking. Companion change: lift the selected tool and its form values into App.tsx alongside the already-lifted toolCallState, make ToolsScreen controlled, and drop the clear-on-unmount effect. Leaving the Tools tab and returning now preserves selection, inputs, and result. All three reset together on disconnect via resetSessionScopedUiState, preserving the #1368/#1394 behavior. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(web): persist per-screen selection/search/filter across navigation (#1417) Lift every screen's selection, form, search, and filter state out of the screen components into session-scoped state owned by App, passed down as controlled props. The screens unmount on tab switch, so this is what keeps the user's context in place when they navigate away and back within a live session. State is cleared only on disconnect (resetSessionScopedUiState) or an explicit user action — never on plain navigation. Lifted per screen: - Tools: sidebar search (selection/form/result were lifted in #1414) - Prompts: selected prompt, argument values, submitted marker, search - Resources: selected resource/template URI, originating-template marker, search, accordion open-sections - Apps: selected app, form values, search (running/maximized stay local — they're tied to the live iframe/bridge that is torn down on unmount) - Tasks: search + status filter - Logs: filter text + visible-level set - History: search + method filter - Network: filter text + visible-category set The four sidebar *Controls (Tool/Prompt/App/Resource) become controlled for search; ResourceControls also controls accordion open-sections. Level/category default maps move to logLevels.ts / fetchCategories.ts so the screen files keep a single component export. Pagination already persists (it lives in the App-level Paged*State managers), so no work was needed there. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(web): persist list scroll position across navigation + reset tests (#1417) Add useScrollMemory: a module-keyed hook that remembers a Mantine ScrollArea's offset across unmount/remount (the screens unmount on tab switch). Scroll lives in a module-scope map rather than threaded App state — it's ephemeral DOM state, and threading ~16 more props would balloon the surface for no benefit. App calls clearScrollMemory() in resetSessionScopedUiState so offsets clear on disconnect alongside the lifted selection/filter state. Wired into the four main list panels (Logs, History, Network, Tasks) and the Tools/Prompts/Apps sidebar lists. Pagination already persists via the App-level Paged*State managers. Tests: useScrollMemory unit test (save/restore, key isolation, clear), and the App disconnect test now also exercises representative lifted UI state (tool search, prompt selection, log filter) clearing on disconnect. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(web): collapse per-screen lifted state into one {screen}Ui object Replace the ~46 individual selection/search/filter props threaded App → InspectorView → screen with one `{screen}Ui` object + one `on{Screen}UiChange` setter per screen (8 pairs). Each screen reads its fields from `ui` and updates via `onUiChange({ ...ui, field })`; the in-flight result panels (toolCallState / getPromptState / readResourceState) stay separate since they're written by App's async action handlers, not the screens. The `*UiState` interfaces live with their screens; the empty-state defaults are gathered in screenUiState.ts so the screen files keep a single component export (react-refresh), mirroring logLevels.ts / fetchCategories.ts. Trims InspectorView's prop surface substantially and keeps the persistence behavior identical. Tests/stories collapse their stateful wrappers to a single ui object; added Tools/Tasks/History interaction tests to keep per-file coverage above the gate. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(web): dismiss visible progress toasts on client teardown (#1414 review) The progressNotification effect cleanup cleared its id-tracking set but left on-screen "Tool progress" toasts to auto-close up to 5s later — they could linger into the next session, and the lingering toast's onClose could delete an id from the new session's set and trigger a duplicate-id re-show. Hide each tracked toast in cleanup so both gaps close. Addresses @claude review note #2. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(web): clear the tool result when a different tool is selected The Tools result panel renders `toolCallState` regardless of which tool is selected, so selecting a new tool used to leave the previous tool's result showing beneath it. Route tools UI changes through an App handler that drops `toolCallState` whenever `selectedToolName` changes; search/form edits keep the selection (and the result) intact. Tests cover both the clear-on-switch and keep-on-search paths. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
8ebd55723f |
fix(dev): handle client disconnect in the dev proxy stream pump (#1418) (#1419)
The Hono dev-proxy middleware pumped streaming /api responses to the Node ServerResponse without handling client disconnect. When the browser dropped a stream mid-flight (SSE close, navigation, HMR reload) `res` was destroyed and the next write failed with ERR_STREAM_DESTROYED, which the pump logged as an error. It also had no `error` listener (a failed write could surface as an uncaught exception) and no `close` listener (it kept reading upstream after the client left). Make the pump disconnect-aware: - Listen for res `close`/`error`; either cancels the upstream reader (resolving the pending read so the loop exits) and flips a `clientGone` flag. - Treat ERR_STREAM_DESTROYED / EPIPE / ECONNRESET as a normal end — not logged. - Only log genuinely unexpected stream errors. - Await each write so a slow client applies backpressure instead of the proxy buffering the whole stream. Dev-only plugin (excluded from the coverage gate — it needs a real Vite server to exercise). Verified via npm run validate. Closes #1418 Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
8f6a2e0f78 |
feat(web): render pending sampling/elicitation requests (#1407) (#1411)
* feat(web): render pending sampling/elicitation requests (#1407) Server-initiated sampling and elicitation requests never reached the v2 web UI: the InspectorClient queued them and returned a Promise that only resolved on a user response, but nothing subscribed to the pending-change events or rendered the (already-built) request components. A tool that triggered one spun the Execute button forever. Wire the queues to the UI: - Add usePendingClientRequests() (core/react) subscribing to pendingSamplesChange / pendingElicitationsChange and returning the live arrays. Expose getPendingSamples() / getPendingElicitations() on InspectorClientProtocol (and the test fake) so hooks can read them. - Add PendingClientRequestModal, an App-level modal that surfaces one queued request at a time (sampling-first) via the existing SamplingRequestPanel / ElicitationFormPanel / ElicitationUrlPanel. It is non-dismissable so the blocked call only resolves via an explicit action. - Wire it in App.tsx: responding/rejecting resolves the client's handler Promise, which unblocks the originating call and clears the spinner. Also fold in two issues surfaced while verifying: - SchemaForm rendered an `array` + `items.enum` (multi-select enum) as a JSON textarea because only `items.anyOf` had a branch; add a MultiSelect branch with enumNames label support. - ToolsScreen's result lives in App and outlived the screen's local selection (reset on tab-switch unmount), leaving a result with no selected tool. Clear the result on unmount for a clean slate on return. Prompts/Resources gate their preview on selection, so they are unaffected. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(elicitation): support explicit decline; honest queue label; review polish Address @claude review on #1411: - Add an explicit "Decline" action to ElicitationFormPanel so the spec's decline (refusal) is distinguishable from cancel (dismissal); the JSDoc claimed accept/decline/cancel but only accept/cancel were wired. Wire the modal body to emit { action: "decline" }. - Replace the misleading "1 of N" queue label (the leading 1 never changed since the modal always shows the head) with an "N pending" hint, hidden when nothing else is queued. - Make the dead useMemo honest: derive the head request inside the memo from the source arrays so it actually caches (the inline activeElicitation had a fresh identity every render). - Document the URL elicitation accept-on-open semantic with a comment pointing at the follow-up (#1415). - Add an App-level round-trip test driving pendingSamplesChange end-to-end (modal opens, respond() fires, empty event closes it). Follow-ups filed: #1415 (URL two-step completion flow), #1416 (form required- field submit gate + double-submit guard). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(elicitation): include untouched schema defaults in submitted content The schema form displays a field's default via resolveValue(), but onChange only writes a field into `values` once the user edits it. So an elicitation submitted with default-only fields left untouched dropped them entirely — v2 sent just the manually-edited fields while v1 sent defaults too (reported with the everything server: firstLine/integer/number/enum defaults missing). Add collectSchemaDefaults() to jsonUtils (recurses nested objects, omits fields with no default) and seed ElicitationFormModalBody's values state with it via a lazy initializer. The modal body is keyed by request id, so it re-seeds per request; user edits still override seeded defaults via the existing onChange merge. Tests: collectSchemaDefaults unit tests (field types, nested, empty) in jsonUtils.test; a modal test asserting an untouched-default form still submits its defaults in content. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(forms): seed schema defaults for tool-call and app-launch forms too Extend the elicitation defaults fix (collectSchemaDefaults) to the other two SchemaForm consumers that own form state and submit it: ToolsScreen (tool execute args) and AppsScreen (app launch args). Both reset formValues to {} on selection, so default-only fields the user never edited were dropped from the submitted args — same root cause as the elicitation modal. Seed formValues from the selected tool/app inputSchema defaults on select. User edits still override via the existing onFormChange merge. (InlineElicitationRequest has no consumers wired up, so it's left as-is.) Tests: ToolsScreen sends a tool's default on Execute without editing; AppsScreen sends an app's default on Open App without editing. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(elicitation): gate form submit on required fields; guard double-submit (#1416) Fold the #1416 hardening into this PR: - Gate the elicitation form's Submit button on required fields. Promote the hasMissingRequiredFields helper out of AppDetailPanel into jsonUtils (shared) and use it in ElicitationFormPanel so Submit is disabled until every required field has a value. - Guard against double-submit. Add an optional `busy` prop to the sampling, elicitation-form, and elicitation-url panels that disables their responding actions. PendingClientRequestModal's bodies use a new ref-guarded useRespondOnce() hook: the first action dispatches and locks the panel; later clicks no-op. This stops a fast double-click from calling respond()/reject() twice (which throws and produced an unhandled rejection). Copy URL stays enabled — it doesn't resolve the request. Tests: hasMissingRequiredFields unit tests; ElicitationFormPanel submit-gate and busy-lock tests; a modal double-click-resolves-once test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(tools): cap detail/result panels at viewport and scroll internally The Tools screen's detail and result columns sized to their content and bled past the bottom of the viewport on long output (e.g. the env dump or a long elicitation result). Cap each column at the screen's available height and let its panel scroll internally instead. - ToolsScreen: drop `align: flex-start` and wrap each column in a stretched `ContentPane` (Flex column, capped by `mah` = viewport − header − xl*2). The pane's definite height is what lets the card's inner ScrollArea know how far to shrink — a bare `mah` on the card does not (the card sizes to content, so the scroll region renders full-height and overflow:hidden just clips it, unreachable). Mirrors the proven Prompts/Resources preview panes. - ToolResultPanel: pinned header + inner ScrollArea (flex 0 1 auto, mih 0). - ToolDetailPanel: pinned title/annotations header, scrolling body (description + form + progress), pinned Execute footer. Audited the other screens: Prompts/Resources already use this pattern, ServerList and the four list panels (Logging/Network/History/Tasks) already cap via ScrollArea.Autosize. Only AppsScreen's app-input form had the same gap — fixed it the same way: AppDetailPanel now scrolls internally and the card uses variant="preview" so the running app's iframe and a long form both stay within the viewport. Verified in-browser against the everything server: the get-env result now stops at the viewport bottom with margin and scrolls within the card (clientHeight 502 vs scrollHeight 1931), header pinned. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(remote): use sync pino destinations in logging tests to fix teardown race CI flaked with an uncaught `ENOENT: ... /tmp/inspector-log-unknown-level-*/app.log` that failed an unrelated later test. The remote-logging integration tests create `pino.destination(logFile)` loggers (async sonic-boom streams) and rmSync the temp dir in finally/afterEach without closing the stream — a buffered background flush then fires after the dir is gone and throws an uncaught error, which Vitest counts as a run failure even though all tests pass. Make those destinations `sync: true` so the write lands during the request instead of on a later flush, removing the race. Test-only; unrelated to this PR's feature work but it was blocking the PR's CI. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
e1c8f71924 |
fix(storage): await store-file writes instead of polling (#1412) (#1413)
* fix(storage): await store-file writes instead of polling (#1412) OAuth persistence is Zustand `persist` → file-storage adapter → writeStoreFile, which the persist middleware invokes fire-and-forget: saveTokens updates the in-memory store synchronously while the file write lags. Tests verified persistence by polling the state file with a 2s/50ms `waitForStateFile()` budget, which flaked under full-suite load (the file was written, the poll just didn't observe it in time). Make the write completion awaitable instead: - Track in-flight writeStoreFile() promises per resolved path in store-io and add flushStoreFileWrites(filePath?) to await them (also useful for graceful shutdown). Writes to the same path are chained so a flush awaits every queued write. - Convert the OAuth E2E, node storage, and file-storage-adapter tests from poll loops (waitForStateFile / vi.waitFor on existsSync / setTimeout) to `await flushStoreFileWrites(path)` + a single read. - Remove the now-unused waitForStateFile() test helper. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(storage): doc load-bearing sync registration; rename stale debug env Address @claude review on #1413: - Document that writeStoreFile()'s synchronous pendingWrites registration is load-bearing for flushStoreFileWrites(), in both the storage adapter's setItem callsite and the pendingWrites docblock, so future refactors don't introduce an await before registration and silently re-flake the tests. - Rename the stale DEBUG_WAIT_FOR_STATE_FILE test env var to DEBUG_PERSIST_PATH (waitForStateFile was removed in this PR). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
8be1680494 |
chore(web): silence benign config-load dep warnings in npm run dev (#1408)
* chore(web): silence benign config-load dep warnings in `npm run dev` `vite dev` printed three UNRESOLVED_IMPORT warnings at startup (chokidar, atomically, @napi-rs/keyring). They originate from Vite's config bundler (Rolldown): the Hono plugin statically imports the node-only dev backend (core/mcp/remote/node/server.ts), so loading vite.config.ts bundles it and Rolldown walks its node-only deps. Those deps are correctly externalized — the browser never loads them and Node resolves them at runtime — so the warnings are pure noise. They can't be filtered the usual ways: the static import is required (the monorepo has no node_modules under core/, so the aliases must resolve the deps at bundle time — ssrLoadModule / `--configLoader runner` both fail to resolve them), and the warnings are written directly to the process streams during config load, bypassing customLogger, logLevel, and rollupOptions.onwarn. Fix: a tiny `--import` hook (server/quiet-config-warnings.mjs) that drops only those specific benign warning writes, wired into the `dev` script. Scoped to `npm run dev` — build, tests, and prod are untouched. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(web): harden dev warning filter per review - Tighten BENIGN_PAIRS to repo-relative path fragments (not basenames) so an unrelated future UNRESOLVED_IMPORT warning mentioning the same dep + a same-named file isn't silently dropped. Verified Rolldown prints the full repo-relative path. (review #1) - Add a note that the matcher tracks Rolldown's message format and degrades to a safe no-op (warnings return) if that format changes. (review #2) - Export isBenignWarning/BENIGN_PAIRS, guard the stream patching behind !VITEST, add a .d.mts so the pure helper is importable from TS, and add a unit test locking the signature pairs and the over-match guards. (review #3) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
5fb2b503f4 |
feat(apps): render running MCP Apps end-to-end (#1370) (#1403)
* feat(apps): render running MCP Apps end-to-end (#1370) Wire the v2 Apps screen so clicking Open App actually runs an MCP App. Previously App.tsx stubbed the three load-bearing pieces (sandbox URL, bridge factory, action handlers), so the iframe loaded about:blank and nothing crossed the bridge. - Add useSandboxUrl hook (core/react) to read sandboxUrl from GET /api/config and thread it down as sandboxPath; render an "MCP Apps unavailable" state instead of a blank iframe when absent. - Add createAppBridgeFactory: builds a host-side ext-apps AppBridge over the active client's SDK client, reads the tool's UI resource on the sandbox handshake and pushes it into the proxy, handles openLinks, and connects a PostMessageTransport. - AppRenderer: pass the tool to the bridge factory; buffer tool input/result and release them once the view signals `initialized` (input before result, latest-wins). - Move the AppRenderer handle ref to App.tsx so onOpenApp issues tools/call with the form values and delivers input + result to the running app via the bridge; onCloseApp tears the bridge/iframe down. - Stories: themed sandbox placeholder (visible in light/dark), an echo-bridge running story, and an unavailable story. New unit tests for the factory, the buffering, useSandboxUrl, and the empty state. The listChanged indicator badge wiring is tracked separately in #1402. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(apps): make MCP Apps render in dev — single bridge + result passthrough Two bugs kept apps (e.g. NightRider's MIDI Pattern Editor) stuck on an empty shell even though the host fetched the resource and called the tool correctly. 1. StrictMode double-mounted the bridge. The dev-only effect setup→cleanup→ setup built TWO host bridges/transports, double-loaded the sandbox, and raced the app's ui/initialize handshake (it timed out, so `initialized` never fired and buffered input/result were never flushed). AppRenderer now defers bridge disposal to a microtask and, when the effect re-runs synchronously with identical inputs, cancels the disposal and reuses the live bridge — so a single bridge survives the double-invoke. The pending input/result buffer is no longer cleared on a transient cleanup. 2. The SDK rejected the server's result. callTool() validates structuredContent against the tool's outputSchema and throws on a mismatch — but an MCP App is a passthrough: the host must forward whatever the server returned to the running view (which legacy hosts render fine). InspectorClient.callTool gains a `skipOutputValidation` option that routes through client.request() (no wrapper validation); onOpenApp uses it. Verified end-to-end against a real app via Playwright: the editor now renders its grid + pattern data identically to the v1 inspector. Tests: AppRenderer StrictMode single-bridge/no-dispose case; a get_temp_extra test-server fixture (returns an extra structuredContent property) with integration tests covering both the throwing default path and the skipOutputValidation passthrough. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(apps): warn (toast) when an app's result violates its outputSchema The skipOutputValidation passthrough keeps schema-mismatched apps rendering, but that silently hid a real spec problem from server developers. Now the host still renders, but surfaces the mismatch. - InspectorClient.callTool runs the SDK's own Ajv output validation on the skipOutputValidation path NON-fatally and returns the message as `outputValidationError` on the ToolCallInvocation (and the toolCallResultChange event); the result is still delivered. - App.tsx onOpenApp shows a yellow toast when outputValidationError is set, explaining strict MCP clients may not render the app. Tests: integration cases asserting outputValidationError is set for a schema-violating result (get_temp_extra) and unset for a valid one (get_temp). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(apps): summarize output-schema warning in toast + details modal The previous toast dumped the full (very long) validation error and overflowed. Now the toast shows a one-line summary with a "View validation details" link that opens a modal containing the full error in a read-only, scrollable textarea. - New OutputValidationModal group component (+ story + tests). - App.tsx: toast message is a summary + link; clicking it opens the modal populated with the tool name and the full outputValidationError. Verified via Playwright against the NightRider MIDI editor: toast is concise, link opens the modal with the scrollable details. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(apps): address PR review — theme stability, onError, validation polish Responds to the automated PR review on #1403. - Theme toggle no longer rebuilds a running app's bridge. createAppBridgeFactory reads the resolved theme from the DOM (Mantine's data-mantine-color-scheme) at bridge-build time instead of capturing isDark in the factory deps, so the factory identity is stable across theme flips. (#1) - Wire AppRenderer's onError through AppsScreen -> InspectorView -> App.tsx, surfacing bridge/runtime failures (no client, late rejection, failed tools/call) as a red toast instead of a silent blank iframe. (#2) - Extract validateToolOutput into a pure, unit-tested helper covering the matching / extra-property / missing-structuredContent / error / bad-schema cases (the no-structuredContent branch can't be produced via the SDK server, which rejects it before sending). (#4) - useSandboxUrl aborts its in-flight fetch on unmount (cancelled flag) to avoid setState after unmount. (#5) - Schema-mismatch toast: autoClose:false (details modal is one click away). (note) - AppRenderer: add a state-machine comment block documenting the bridge lifecycle refs. (#6) - inspectorClient: note why the `as CallToolResult` casts are safe. (note) Tests: AppsScreen onError surface; validateToolOutput unit suite; factory theme read from DOM. validate + test:storybook green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(apps): clarify theme attribute + useSandboxUrl fetch comments Re-review follow-ups (non-blocking): - createAppBridgeFactory: note that Mantine's data-mantine-color-scheme is only ever light/dark (auto is resolved before paint), so no `auto` branch is needed and matchMedia only covers an absent attribute. - useSandboxUrl: correct the doc to say it fetches on mount AND re-fetches on baseUrl/authToken change (idempotent), not "once on mount". Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
440b194f64 |
fix(state): dispatch connect event after capabilities are populated (#1405)
* fix(state): dispatch connect event after capabilities are populated The list-state managers (tools/prompts/resources/templates) refresh on the "connect" event and, since #1395, gate their list RPC on getCapabilities(). But connect() dispatched "connect" before fetchServerInfo() populated this.capabilities, and refresh() runs synchronously up to its first await — so the capability gate executed during the synchronous dispatch, read undefined capabilities (reset to undefined on every disconnect), tripped, and wiped every list to empty on connect. Tools, prompts, resources, and resource templates all vanished against any server. Move the "connect" dispatch to after the awaited fetchServerInfo() so capabilities are in place before any manager refreshes. The unit tests missed this because their FakeInspectorClient has capabilities populated from construction; added an integration regression test wiring a real ManagedToolsState on connect and asserting the tool list is non-empty (fails without the fix). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(integration): harden connect-race regression test per review Address the two non-blocking suggestions from the PR review: - Replace the 100ms settle sleep with waitForEvent() on the managers' change events, so the test is deterministic against slow CI instead of racing a fixed timeout. - Assert a second affected primitive (ManagedResourcesState alongside ManagedToolsState). The fix is a single shared line, but covering two distinct capabilities guards against a future change gating one primitive differently. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
8b4375584a |
refactor(app): extract resetSessionScopedUiState() helper (#1394) (#1398)
Pull the four session-scoped UI resets (toolCallState / getPromptState / readResourceState panels + optimistic currentLogLevel) out of the disconnect listener into a colocated `resetSessionScopedUiState` useCallback, so there's a single named place to extend as App.tsx accrues more per-session state. The disconnect listener keeps its activeServerId + modal-flag bookkeeping inline and calls the helper. Pure refactor — no behavior change; the existing App.test.tsx disconnect test (asserts all four pieces reset) is unchanged and still passes. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
9e494544e7 |
fix(state): gate list fetches on server capability (#1350) (#1395)
The four list-fetching state managers fired their list RPC against every server on connect regardless of advertised capabilities, so a server that doesn't implement a given primitive replied -32601 "Method not found", surfacing in the console on every connect. Mirrors the `tasks`-capability gate added for ManagedRequestorTasksState in #1349. - managedToolsState → gate on capabilities.tools - managedPromptsState → gate on capabilities.prompts - managedResourcesState → gate on capabilities.resources - managedResourceTemplatesState → gate on capabilities.resources (the spec defines no separate resourceTemplates capability; resources/templates/list is part of the resources surface) When the capability is absent each manager sets an empty list, dispatches its change event, and returns — the right semantics for "this server doesn't support X." Tests: existing flow tests now construct their FakeInspectorClient with the relevant capability so the live list path is still exercised; added a "refresh skips listX when capability absent" and a "connect against an X-less server doesn't fire listX" test per manager. Updated the useManagedX peers and resourceSubscriptionsState (which drives a ManagedResourcesState refresh) to advertise the capability too. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
905b80a88c |
fix(app): clear per-call panels and log level on disconnect (#1368) (#1392)
* fix(app): clear per-call panels and log level on disconnect (#1368) Session-scoped UI state owned by App.tsx — the Tools / Prompts / Resources result panels (`toolCallState` / `getPromptState` / `readResourceState`) and the optimistic `currentLogLevel` — survived a disconnect/reconnect cycle, so server B's screens showed server A's last result. Extend the existing `disconnect` listener to reset all four back to empty / "info", alongside the activeServerId + modal-flag resets it already does. This covers all three disconnect paths the listener handles (explicit toggle, header Disconnect button, mid-session transport failure). `latencyMs` continues to reset via the connectionStatus effect. Adds App.test.tsx covering the disconnect → tool-call-panel-cleared and log-level-reset transition, mocking the InspectorClient (a fake EventTarget), the per-server state managers, the core hooks, and InspectorView. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(app): assert all four panels reset on disconnect (#1368) Address PR review: the disconnect test asserted only toolCallState and currentLogLevel. Extend the InspectorView double with prompt/resource status spans and get-prompt/read-resource buttons so the test fills and then asserts the reset of all four pieces of session-scoped state (tool / prompt / resource panels + log level), fully matching the "reset on disconnect" contract. Retitle the test accordingly. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
10f429759f |
chore(deps): npm audit fix for transitive security advisories (#1380)
Resolves 15 of 16 Dependabot alerts via lockfile-only updates (no breaking major bumps): handlebars (critical), fast-uri, flatted, hono, path-to-regexp, rollup, vite, express-rate-limit, @hono/node-server, ajv, brace-expansion, ip-address, picomatch, postcss, qs, ws, yaml, and @tootallnate/once. The remaining minimatch@3.1.2 advisory comes from @eslint/config-array (dev/lint-only, not shipped) and will clear when ESLint bumps it. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
5c5e5ab754 |
feat(network): show auth response bodies with masked secrets + reveal toggle (#1386) (#1387)
* feat(auth): inject MCP_INSPECTOR_API_TOKEN into served index.html (#1378) Reloading the web client at the bare URL (no `?MCP_INSPECTOR_API_TOKEN=…` query string) with empty sessionStorage made every `/api/*` request 401 — the browser had no way to recover the backend's auth token. Embed the token into `index.html` on every page load so the browser no longer depends on the query string surviving navigation: - New shared helper `clients/web/server/inject-auth-token.ts` embeds `<script>window.__INSPECTOR_API_TOKEN__ = "…"</script>` (escaped against `</script>` injection; no-op when auth is dangerously omitted). - Dev: the Vite plugin injects via `transformIndexHtml`. - Prod: the Hono server injects on the `/` route. - `App.tsx` `getAuthToken()` now reads the injected global first, then the query string, then sessionStorage (both fallbacks preserved). - Shared global name lives in `INSPECTOR_API_TOKEN_GLOBAL` (`core/mcp/remote/constants.ts`). Tests: helper unit coverage + an integration test exercising the real prod server's `/` → `/api/*` flow (injected token authenticates; missing token 401s). AGENTS.md documents the token-recovery order. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(auth): wire OAuth authorization-code flow into App.tsx (#1379) OAuth-protected MCP servers could not be connected to from the v2 web client: the core OAuth pipeline exists, but App.tsx never invoked it, so a connect attempt 401'd and surfaced "Remote send failed (401): … Missing Authorization header" as a toast. Wire the two missing entry points (all core primitives already in place): - Auto-trigger on 401: in onToggleConnection's catch, detect an upstream 401 (isUnauthorizedError) and call client.authenticate(), which runs discovery + DCR (backend-proxied) and redirects the page to the auth server via BrowserNavigation. The initiating server id is persisted to sessionStorage first, since the OAuth `state` carries only mode+authId and the full-page redirect wipes React state. - /oauth/callback handler: a mount effect that, once `servers` hydrate, parses the callback params, recovers the pending server, rebuilds its InspectorClient, runs completeOAuthFlow(code) (PKCE verifier + DCR client info survive in BrowserOAuthStorage), replaceState("/") so a reload can't replay the single-use code, then connect(). An `error=` callback toasts instead of retrying. connect() already attaches the OAuth provider to the transport (inspectorClient.ts), so once tokens land in BrowserOAuthStorage the outbound request carries the bearer token. Extracted the pure pieces (constants + isUnauthorizedError) to src/utils/oauthFlow.ts with unit tests. Verified end-to-end in a real browser against the MCP SDK demo OAuth server: Connect -> redirect -> auto-approve -> callback -> Connected, with the access token shown in the Connection Info modal (#1377). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(auth): persist OAuth pre-redirect Network log across the redirect (#1384) After #1379, the Network tab showed only the post-redirect auth HTTP (discovery re-run + POST /token); the pre-redirect discovery and the DCR POST /register that run during authenticate() were lost when the page navigated to the auth server. Root causes: 1. Ordering — BrowserNavigation set `window.location.href` before the client's `saveSession` event fired (OAuthManager calls onBeforeOAuthRedirect *after* auth() already navigated), so the save raced the unload and was dropped. Fix: BrowserNavigation now runs a synchronous `beforeNavigate` hook immediately before assigning location.href; App wires it through createWebEnvironment to flush the active fetch log to RemoteInspectorClient Storage (keyed by the authId parsed from the auth URL) via a keepalive POST that outlives the unload. 2. Illegal invocation — RemoteInspectorClientStorage defaulted to `this.fetchFn = globalThis.fetch` and called `this.fetchFn(...)`, which re-binds `this` and makes native fetch throw "Illegal invocation" (swallowed by the catch). This silently broke *all* session save/load. Fix: default to a wrapper that preserves the global receiver. 3. Restore race — hydrateFetchRequests replaced the list, so a load that resolved after the resuming connect appended live entries would clobber them. Fix: merge restored (older) entries ahead of live ones, dedupe by id. saveSession also now uses keepalive: true. Verified end-to-end against the MCP SDK demo OAuth server: the connected page's Network tab shows the full handshake — pre-redirect discovery + DCR /register plus post-redirect discovery + /token as `auth`, alongside `transport`. Added unit tests for the beforeNavigate ordering and the hydrate merge/dedupe. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(network): show auth response bodies with masked secrets + reveal toggle (#1386) Auth-category Network entries showed request body/headers/status but never the response body (rendered "(empty)"), because buildEffectiveAuthFetch deliberately skipped capturing it to avoid surfacing access_token / refresh_token. That hid the most useful thing to inspect when debugging OAuth — the token exchange. Capture auth response bodies, but mask sensitive OAuth fields by default behind a click-to-reveal toggle so they aren't exposed at a glance during a screen-share: - inspectorClient: wire updateResponseBody on the auth fetcher. - src/utils/maskSecrets.ts: maskSecretsInBody() masks access_token, refresh_token, id_token, client_secret (case-insensitive, nested) in JSON bodies; reports whether anything was masked. Non-JSON / secret-free bodies pass through untouched. - NetworkEntry BodyPreview: when a body has masked fields, render it masked with a Reveal/Hide toggle (copy honors the shown view). Masking is a UI concern; the raw entry is unchanged so reveal shows the real value. access_token / refresh_token live in the post-redirect /token response, which is never written to the session-restore files (#1384); only pre-redirect bodies (public discovery, DCR /register) persist, so no bearer token hits disk. Verified end-to-end: the /token response shows masked by default (access_token: "••••••••"), Reveal exposes the raw value, and discovery / the public DCR /register response (no secret) render with no toggle. Added util + component unit tests and a story play function. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(auth): inject token into prod SPA fallback + prime sessionStorage + no-store (#1378) Addresses code-review feedback on the token-injection PR: - Prod `server.ts` SPA deep-link fallback (e.g. `/oauth/callback`) previously served the raw index.html off disk via serveStatic, bypassing injection — so a bookmark/reload at a non-`/` route with empty sessionStorage would 401. Route the SPA fallback through the same `serveIndexHtml` (inject) helper; real static assets (paths with a dot) still serve verbatim. Dev already injected on every HTML serve via Vite `transformIndexHtml`. - `getAuthToken()` now persists the injected `window.__INSPECTOR_API_TOKEN__` to sessionStorage (not just the URL-param branch), priming the backstop for any later navigation that loses the global. - Injected HTML responses now send `Cache-Control: no-store`, so a page carrying a token isn't cached and served stale after a restart regenerates the token. Integration tests added: SPA fallback (`/oauth/callback`) carries the token, `Cache-Control: no-store` on injected HTML, real assets served verbatim, and unknown `/api` routes 404 rather than falling through to the HTML shell. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(auth): address #1383 review — split OAuth/connect toasts, tighten 401 match (#1379) Code-review feedback on the OAuth-wiring PR: - Callback effect: split completeOAuthFlow vs connect() into separate try/catch blocks. A token-exchange failure now reads "OAuth token exchange failed … Please try connecting again." (the single-use code is spent and the URL was cleared, so a reload can't retry); a post-OAuth connect failure reads "Failed to connect" since OAuth actually succeeded and re-clicking Connect reuses the persisted tokens. - isUnauthorizedError: anchor the message fallback on the transport's `failed …(401)` wording instead of a bare `(401)`, so an unrelated `(401)` spliced into an error message can't trip the OAuth flow. Added a test. - Documented that clearing the pending id + URL before the server lookup is intentional (deleted/renamed server mid-flow → require a fresh Connect). Also merges the squash-merged #1382 base from v2/main. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(auth): address #1385 review — clarify double-save + keepalive cap + test fetch default (#1384) Code-review feedback on the OAuth Network-log persistence PR: - Documented the double-save: `FetchRequestLogState`'s `saveSession` listener is the backstop; `BrowserNavigation`'s `beforeNavigate` hook is the primary flush for the redirect case. Notes the listener may lose the navigation race and is harmless when it duplicates (last-writer-wins, identical payload). - Reworded the keepalive comment in `RemoteInspectorClientStorage.saveSession`: the 64KB cap is general (the method is also reachable from the listener with the full session log), so a long session could exceed it and drop silently — acceptable since the persisted log is best-effort, not load-bearing. - Added a regression test that constructs `RemoteInspectorClientStorage` without a `fetchFn`, stubs `globalThis.fetch`, and asserts the default wrapper calls it (locks in the "Illegal invocation" fix, which callers otherwise swallow). Optional items (logger.warn on swallowed save errors; setupClientForServer dep churn) acknowledged on the PR, not changed. Also merges the squash-merged #1383 base from v2/main. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(network): address #1387 review — form-encoded masking, cleaner flag, a11y (#1386) Code-review feedback on the auth-response-body masking PR: - Extended masking to form-urlencoded bodies (finding #1): the token *request* is `application/x-www-form-urlencoded` and carries `code` / `code_verifier` / `client_secret` / `refresh_token`. `maskSecretsInBody` now masks those in form bodies too (preserving formatting; placeholder not percent-encoded). `code`/`code_verifier`/`client_assertion` are form-only sensitive keys — deliberately NOT masked in JSON, where `code` is usually an error/status code. - Replaced the double-stringify `hasSecrets` heuristic with an explicit masked-flag propagated out of `maskNode` (finding #2) — robust if the transform ever grows non-identity behavior, and one fewer serialization. - Reset reveal state on body change via `key={body}` remount instead of a setState-in-effect (finding #3; avoids the cascading-render lint rule). - a11y (finding #4): `aria-label` on the Reveal/Hide button and `aria-live` on the hidden/revealed status text. - Security tripwire comment near the `saveSession` listener (finding #6): captured auth bodies are unmasked at source (masking is UI-only), so any new post-token-exchange persistence path must redact first. Tests: form-encoded masking (token + refresh requests), JSON `code` NOT masked, non-object JSON pass-through, default-fetch wrapper. Story play + NetworkEntry tests updated for the new aria-labels and the now-masked form request body. Finding #5 (pretty-print asymmetry) is already handled by ContentViewer's JSON formatting; #4-aria and #6 are the doc/a11y bits. Also merges the squash-merged #1385 base from v2/main. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(network): address #1387 third-pass review — content-type masking, wholesale mask, dedup (#1386) Third-pass code-review feedback on the auth-body masking PR: - maskSecretsInBody now takes the body's content-type (finding #1): `*json*` → JSON masking, form-urlencoded → form masking, any other known type (HTML/plaintext/XML) → no masking; absent/unknown → sniff as before. Removes the implicit "non-JSON ⇒ form" guess for error pages etc. NetworkEntry passes the request/response `content-type` header through to BodyPreview. - Mask any non-empty value under a sensitive key wholesale (finding #2): a non-standard object/array value under e.g. `access_token` is replaced rather than recursed into, so it can't leak. Empty-string still not flagged. - Extracted `isSensitiveKey(set, key)` to dedupe the JSON/form key checks (finding #3). - Reworded `MaskResult.masked` doc to cover the form (non-pretty-printed) case (finding #6). Tests: non-string-value-under-sensitive-key wholesale mask; repeated form param; empty form value not flagged; content-type honored (JSON type skips form masking, text/plain skips masking, explicit form type masks). Finding #5 (story re-hide) skipped per reviewer — unit test already covers re-mask. Finding #4 (form edge cases) added. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(network): address #1387 fourth-pass nits — content-type contract, key, form test (#1386) Fourth-pass review (LGTM) — minor, non-blocking items: - Documented the content-type matching contract on `maskSecretsInBody`: substring match (`*json*` / `*x-www-form-urlencoded*`), and we trust the wire's own label (a mislabeled body renders raw — acceptable for the screen-share threat model) (finding #1). - Key `<BodyPreview>` by content-type + body (not body alone) so a header-only change would also reset the reveal state (finding #3; not reachable today). - Storybook `AuthSuccess`: assert revealed count `>=` reveal-button count to mirror the `hidden >= 2` check, so adding a non-masked body later can't drift the assertion silently (finding #4). - Added a `NetworkEntry` unit test for form-encoded request-body masking (code/code_verifier) — previously exercised only via the Storybook play. Finding #2 (silent decodeURIComponent fallback on malformed keys) acknowledged as a known quirk outside the threat model; no change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * perf(network): address #1387 fifth-pass — memoize masking, mask DCR mgmt token (#1386) Fifth-pass review (LGTM): - Memoized `maskSecretsInBody` in `BodyPreview` (recommended item #1): a Reveal/Hide click no longer re-parses + re-walks the body; cost is once per mount, and the `key` remount re-runs it on body/content-type change. Guarded so a too-large body is never parsed (the hook runs unconditionally, with the size check inside the memo). - Added `registration_access_token` (RFC 7592 DCR management credential, same bearer class) to the masked key set, and a confidential-client `/register` response fixture test (#5) asserting client_secret + registration_access_token masked, client_id/metadata visible. - Tightened the `isMaskableValue` doc to state the exact contract: non-null, non-empty-string values are masked wholesale (#3). Items #2 (transport parse cost — capped by the memo) and #4 (test-only MASK_PLACEHOLDER export) left as-is per the review. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(network): clarify body-update emits list event only (#1387 sixth pass) (#1386) Sixth-pass review (LGTM, all nits). Added a comment on `onFetchRequestBodyUpdate` noting it re-emits only `fetchRequestsChange`, not the per-entry `fetchRequest` event — list-reading consumers pick up the body on the next render; a future per-entry subscriber would need its own event. Other items left as-is per the review: full-string `BodyPreview` key (#1, micro-perf only if profiled), NaN under a sensitive key (#3, consistent with the mask-non-null-non-empty contract), `&`-only form separator (#4, OAuth never emits `;`). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
98192fd86e |
fix(auth): persist OAuth pre-redirect Network log across the redirect (#1384) (#1385)
* feat(auth): inject MCP_INSPECTOR_API_TOKEN into served index.html (#1378) Reloading the web client at the bare URL (no `?MCP_INSPECTOR_API_TOKEN=…` query string) with empty sessionStorage made every `/api/*` request 401 — the browser had no way to recover the backend's auth token. Embed the token into `index.html` on every page load so the browser no longer depends on the query string surviving navigation: - New shared helper `clients/web/server/inject-auth-token.ts` embeds `<script>window.__INSPECTOR_API_TOKEN__ = "…"</script>` (escaped against `</script>` injection; no-op when auth is dangerously omitted). - Dev: the Vite plugin injects via `transformIndexHtml`. - Prod: the Hono server injects on the `/` route. - `App.tsx` `getAuthToken()` now reads the injected global first, then the query string, then sessionStorage (both fallbacks preserved). - Shared global name lives in `INSPECTOR_API_TOKEN_GLOBAL` (`core/mcp/remote/constants.ts`). Tests: helper unit coverage + an integration test exercising the real prod server's `/` → `/api/*` flow (injected token authenticates; missing token 401s). AGENTS.md documents the token-recovery order. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(auth): wire OAuth authorization-code flow into App.tsx (#1379) OAuth-protected MCP servers could not be connected to from the v2 web client: the core OAuth pipeline exists, but App.tsx never invoked it, so a connect attempt 401'd and surfaced "Remote send failed (401): … Missing Authorization header" as a toast. Wire the two missing entry points (all core primitives already in place): - Auto-trigger on 401: in onToggleConnection's catch, detect an upstream 401 (isUnauthorizedError) and call client.authenticate(), which runs discovery + DCR (backend-proxied) and redirects the page to the auth server via BrowserNavigation. The initiating server id is persisted to sessionStorage first, since the OAuth `state` carries only mode+authId and the full-page redirect wipes React state. - /oauth/callback handler: a mount effect that, once `servers` hydrate, parses the callback params, recovers the pending server, rebuilds its InspectorClient, runs completeOAuthFlow(code) (PKCE verifier + DCR client info survive in BrowserOAuthStorage), replaceState("/") so a reload can't replay the single-use code, then connect(). An `error=` callback toasts instead of retrying. connect() already attaches the OAuth provider to the transport (inspectorClient.ts), so once tokens land in BrowserOAuthStorage the outbound request carries the bearer token. Extracted the pure pieces (constants + isUnauthorizedError) to src/utils/oauthFlow.ts with unit tests. Verified end-to-end in a real browser against the MCP SDK demo OAuth server: Connect -> redirect -> auto-approve -> callback -> Connected, with the access token shown in the Connection Info modal (#1377). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(auth): persist OAuth pre-redirect Network log across the redirect (#1384) After #1379, the Network tab showed only the post-redirect auth HTTP (discovery re-run + POST /token); the pre-redirect discovery and the DCR POST /register that run during authenticate() were lost when the page navigated to the auth server. Root causes: 1. Ordering — BrowserNavigation set `window.location.href` before the client's `saveSession` event fired (OAuthManager calls onBeforeOAuthRedirect *after* auth() already navigated), so the save raced the unload and was dropped. Fix: BrowserNavigation now runs a synchronous `beforeNavigate` hook immediately before assigning location.href; App wires it through createWebEnvironment to flush the active fetch log to RemoteInspectorClient Storage (keyed by the authId parsed from the auth URL) via a keepalive POST that outlives the unload. 2. Illegal invocation — RemoteInspectorClientStorage defaulted to `this.fetchFn = globalThis.fetch` and called `this.fetchFn(...)`, which re-binds `this` and makes native fetch throw "Illegal invocation" (swallowed by the catch). This silently broke *all* session save/load. Fix: default to a wrapper that preserves the global receiver. 3. Restore race — hydrateFetchRequests replaced the list, so a load that resolved after the resuming connect appended live entries would clobber them. Fix: merge restored (older) entries ahead of live ones, dedupe by id. saveSession also now uses keepalive: true. Verified end-to-end against the MCP SDK demo OAuth server: the connected page's Network tab shows the full handshake — pre-redirect discovery + DCR /register plus post-redirect discovery + /token as `auth`, alongside `transport`. Added unit tests for the beforeNavigate ordering and the hydrate merge/dedupe. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(auth): inject token into prod SPA fallback + prime sessionStorage + no-store (#1378) Addresses code-review feedback on the token-injection PR: - Prod `server.ts` SPA deep-link fallback (e.g. `/oauth/callback`) previously served the raw index.html off disk via serveStatic, bypassing injection — so a bookmark/reload at a non-`/` route with empty sessionStorage would 401. Route the SPA fallback through the same `serveIndexHtml` (inject) helper; real static assets (paths with a dot) still serve verbatim. Dev already injected on every HTML serve via Vite `transformIndexHtml`. - `getAuthToken()` now persists the injected `window.__INSPECTOR_API_TOKEN__` to sessionStorage (not just the URL-param branch), priming the backstop for any later navigation that loses the global. - Injected HTML responses now send `Cache-Control: no-store`, so a page carrying a token isn't cached and served stale after a restart regenerates the token. Integration tests added: SPA fallback (`/oauth/callback`) carries the token, `Cache-Control: no-store` on injected HTML, real assets served verbatim, and unknown `/api` routes 404 rather than falling through to the HTML shell. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(auth): address #1383 review — split OAuth/connect toasts, tighten 401 match (#1379) Code-review feedback on the OAuth-wiring PR: - Callback effect: split completeOAuthFlow vs connect() into separate try/catch blocks. A token-exchange failure now reads "OAuth token exchange failed … Please try connecting again." (the single-use code is spent and the URL was cleared, so a reload can't retry); a post-OAuth connect failure reads "Failed to connect" since OAuth actually succeeded and re-clicking Connect reuses the persisted tokens. - isUnauthorizedError: anchor the message fallback on the transport's `failed …(401)` wording instead of a bare `(401)`, so an unrelated `(401)` spliced into an error message can't trip the OAuth flow. Added a test. - Documented that clearing the pending id + URL before the server lookup is intentional (deleted/renamed server mid-flow → require a fresh Connect). Also merges the squash-merged #1382 base from v2/main. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(auth): address #1385 review — clarify double-save + keepalive cap + test fetch default (#1384) Code-review feedback on the OAuth Network-log persistence PR: - Documented the double-save: `FetchRequestLogState`'s `saveSession` listener is the backstop; `BrowserNavigation`'s `beforeNavigate` hook is the primary flush for the redirect case. Notes the listener may lose the navigation race and is harmless when it duplicates (last-writer-wins, identical payload). - Reworded the keepalive comment in `RemoteInspectorClientStorage.saveSession`: the 64KB cap is general (the method is also reachable from the listener with the full session log), so a long session could exceed it and drop silently — acceptable since the persisted log is best-effort, not load-bearing. - Added a regression test that constructs `RemoteInspectorClientStorage` without a `fetchFn`, stubs `globalThis.fetch`, and asserts the default wrapper calls it (locks in the "Illegal invocation" fix, which callers otherwise swallow). Optional items (logger.warn on swallowed save errors; setupClientForServer dep churn) acknowledged on the PR, not changed. Also merges the squash-merged #1383 base from v2/main. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
9622e8149f |
feat(auth): wire OAuth authorization-code flow into App.tsx (#1379) (#1383)
* feat(auth): inject MCP_INSPECTOR_API_TOKEN into served index.html (#1378) Reloading the web client at the bare URL (no `?MCP_INSPECTOR_API_TOKEN=…` query string) with empty sessionStorage made every `/api/*` request 401 — the browser had no way to recover the backend's auth token. Embed the token into `index.html` on every page load so the browser no longer depends on the query string surviving navigation: - New shared helper `clients/web/server/inject-auth-token.ts` embeds `<script>window.__INSPECTOR_API_TOKEN__ = "…"</script>` (escaped against `</script>` injection; no-op when auth is dangerously omitted). - Dev: the Vite plugin injects via `transformIndexHtml`. - Prod: the Hono server injects on the `/` route. - `App.tsx` `getAuthToken()` now reads the injected global first, then the query string, then sessionStorage (both fallbacks preserved). - Shared global name lives in `INSPECTOR_API_TOKEN_GLOBAL` (`core/mcp/remote/constants.ts`). Tests: helper unit coverage + an integration test exercising the real prod server's `/` → `/api/*` flow (injected token authenticates; missing token 401s). AGENTS.md documents the token-recovery order. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(auth): wire OAuth authorization-code flow into App.tsx (#1379) OAuth-protected MCP servers could not be connected to from the v2 web client: the core OAuth pipeline exists, but App.tsx never invoked it, so a connect attempt 401'd and surfaced "Remote send failed (401): … Missing Authorization header" as a toast. Wire the two missing entry points (all core primitives already in place): - Auto-trigger on 401: in onToggleConnection's catch, detect an upstream 401 (isUnauthorizedError) and call client.authenticate(), which runs discovery + DCR (backend-proxied) and redirects the page to the auth server via BrowserNavigation. The initiating server id is persisted to sessionStorage first, since the OAuth `state` carries only mode+authId and the full-page redirect wipes React state. - /oauth/callback handler: a mount effect that, once `servers` hydrate, parses the callback params, recovers the pending server, rebuilds its InspectorClient, runs completeOAuthFlow(code) (PKCE verifier + DCR client info survive in BrowserOAuthStorage), replaceState("/") so a reload can't replay the single-use code, then connect(). An `error=` callback toasts instead of retrying. connect() already attaches the OAuth provider to the transport (inspectorClient.ts), so once tokens land in BrowserOAuthStorage the outbound request carries the bearer token. Extracted the pure pieces (constants + isUnauthorizedError) to src/utils/oauthFlow.ts with unit tests. Verified end-to-end in a real browser against the MCP SDK demo OAuth server: Connect -> redirect -> auto-approve -> callback -> Connected, with the access token shown in the Connection Info modal (#1377). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(auth): inject token into prod SPA fallback + prime sessionStorage + no-store (#1378) Addresses code-review feedback on the token-injection PR: - Prod `server.ts` SPA deep-link fallback (e.g. `/oauth/callback`) previously served the raw index.html off disk via serveStatic, bypassing injection — so a bookmark/reload at a non-`/` route with empty sessionStorage would 401. Route the SPA fallback through the same `serveIndexHtml` (inject) helper; real static assets (paths with a dot) still serve verbatim. Dev already injected on every HTML serve via Vite `transformIndexHtml`. - `getAuthToken()` now persists the injected `window.__INSPECTOR_API_TOKEN__` to sessionStorage (not just the URL-param branch), priming the backstop for any later navigation that loses the global. - Injected HTML responses now send `Cache-Control: no-store`, so a page carrying a token isn't cached and served stale after a restart regenerates the token. Integration tests added: SPA fallback (`/oauth/callback`) carries the token, `Cache-Control: no-store` on injected HTML, real assets served verbatim, and unknown `/api` routes 404 rather than falling through to the HTML shell. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(auth): address #1383 review — split OAuth/connect toasts, tighten 401 match (#1379) Code-review feedback on the OAuth-wiring PR: - Callback effect: split completeOAuthFlow vs connect() into separate try/catch blocks. A token-exchange failure now reads "OAuth token exchange failed … Please try connecting again." (the single-use code is spent and the URL was cleared, so a reload can't retry); a post-OAuth connect failure reads "Failed to connect" since OAuth actually succeeded and re-clicking Connect reuses the persisted tokens. - isUnauthorizedError: anchor the message fallback on the transport's `failed …(401)` wording instead of a bare `(401)`, so an unrelated `(401)` spliced into an error message can't trip the OAuth flow. Added a test. - Documented that clearing the pending id + URL before the server lookup is intentional (deleted/renamed server mid-flow → require a fresh Connect). Also merges the squash-merged #1382 base from v2/main. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
881ba4b5e8 |
feat(auth): inject MCP_INSPECTOR_API_TOKEN into served index.html (#1378) (#1382)
* feat(auth): inject MCP_INSPECTOR_API_TOKEN into served index.html (#1378) Reloading the web client at the bare URL (no `?MCP_INSPECTOR_API_TOKEN=…` query string) with empty sessionStorage made every `/api/*` request 401 — the browser had no way to recover the backend's auth token. Embed the token into `index.html` on every page load so the browser no longer depends on the query string surviving navigation: - New shared helper `clients/web/server/inject-auth-token.ts` embeds `<script>window.__INSPECTOR_API_TOKEN__ = "…"</script>` (escaped against `</script>` injection; no-op when auth is dangerously omitted). - Dev: the Vite plugin injects via `transformIndexHtml`. - Prod: the Hono server injects on the `/` route. - `App.tsx` `getAuthToken()` now reads the injected global first, then the query string, then sessionStorage (both fallbacks preserved). - Shared global name lives in `INSPECTOR_API_TOKEN_GLOBAL` (`core/mcp/remote/constants.ts`). Tests: helper unit coverage + an integration test exercising the real prod server's `/` → `/api/*` flow (injected token authenticates; missing token 401s). AGENTS.md documents the token-recovery order. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(auth): inject token into prod SPA fallback + prime sessionStorage + no-store (#1378) Addresses code-review feedback on the token-injection PR: - Prod `server.ts` SPA deep-link fallback (e.g. `/oauth/callback`) previously served the raw index.html off disk via serveStatic, bypassing injection — so a bookmark/reload at a non-`/` route with empty sessionStorage would 401. Route the SPA fallback through the same `serveIndexHtml` (inject) helper; real static assets (paths with a dot) still serve verbatim. Dev already injected on every HTML serve via Vite `transformIndexHtml`. - `getAuthToken()` now persists the injected `window.__INSPECTOR_API_TOKEN__` to sessionStorage (not just the URL-param branch), priming the backstop for any later navigation that loses the global. - Injected HTML responses now send `Cache-Control: no-store`, so a page carrying a token isn't cached and served stale after a restart regenerates the token. Integration tests added: SPA fallback (`/oauth/callback`) carries the token, `Cache-Control: no-store` on injected HTML, real assets served verbatim, and unknown `/api` routes 404 rather than falling through to the HTML shell. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
e3b0b6a908 |
feat(connection-info): wire Connection Info modal to the connection handshake (#1377)
* feat(server-info): wire Server Info modal to the connection handshake Closes #1364 - Add ServerInfoModal wrapping the existing ServerInfoContent, mirroring the ServerSettingsModal layout. - Gate the Server Info button on ServerCard to render only when the connection is "connected". - Expose `getClientCapabilities()` on InspectorClient (and protocol + FakeInspectorClient), and surface it through useInspectorClient so the modal can render the client side of the capability handshake without poking the SDK Client's private state. - App.tsx: open the modal from the gated Server Info button, derive the active server's transport and OAuth details (synchronously, from the guided-OAuth state snapshot + persisted settings), and reset the open flag on InspectorClient `disconnect` so the modal does not auto-reopen on a future reconnect. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(server-info): dedupe modal header and render instructions in ContentViewer - Drop the in-content "Server Information" Title from ServerInfoContent and rename the modal title to "Server Information" so the modal renders one heading instead of two stacked ones. - Swap the instructions Blockquote for ContentViewer (copyable, with the existing wrapping Code variant) so long instructions wrap and the user can copy the full payload. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(server-info): rename ServerInfo -> ConnectionInfo The popup covers server identity AND client capabilities AND OAuth state — it's a snapshot of the live connection, not just the server. Rename to match what it actually shows. Also add a "Server Implementation" heading above the top section (name/version/protocol/transport) to balance the existing capability headings. - ServerInfoContent -> ConnectionInfoContent (directory, file, exports) - ServerInfoModal -> ConnectionInfoModal (directory, file, exports) - ServerCard button label "Server Info" -> "Connection Info"; prop onServerInfo -> onConnectionInfo - Modal title -> "Connection Info" - App.tsx / ServerListScreen / InspectorView wiring updated to match Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(connection-info): cap instructions block height so it scrolls in place A long server prompt (e.g. an Everything-style instruction document) was pushing the OAuth section and the modal chrome off-screen — users had to scroll the whole modal to reach the close button. Wrap the ContentViewer in ScrollArea.Autosize with mah=280 so the instructions section scrolls internally and the rest of the modal stays put. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(connection-info): add subtle background to section headings Add a `section` variant for Title that paints the heading in the existing `--inspector-surface-subtle` token (same surface token already used for modal call-out panels). Apply it to each section heading in ConnectionInfoContent (Server Implementation, Server Capabilities, Client Capabilities, Server Instructions, OAuth Details) via a SectionHeading subcomponent constant, so the sections read as distinct cards against the modal background instead of bleeding into one stream of text. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(stories): wrap long import line in ConnectionInfoModal.stories.tsx Prettier flagged the import in CI. The line exceeded printWidth=80 and needed to wrap onto multiple lines. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(agents): require `npm run format` before committing Add a pre-commit instruction to run the auto-fixing `npm run format` ahead of `npm run validate`. `validate` runs `format:check` (non-fixing) which will fail CI on any unformatted file — running the auto-fixer first eliminates that class of failure entirely. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(favicon): use theme/assets/MCP.svg as the favicon source of truth Point index.html's icon link at /src/theme/assets/MCP.svg so the same MCP logo asset already imported by ViewHeader is also the favicon. Delete the now-unused clients/web/public/favicon.svg. Vite resolves the source path during build and emits a hashed copy under dist/assets/. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(server-card): surface failed-handshake errors with a 5s auto-dismiss Two related fixes so the user actually sees what went wrong when a connect attempt fails (e.g. OAuth-backed server, bad URL, missing dev auth token): - App.tsx: add `connectInFlightRef` and skip clearing `activeServerId` in the InspectorClient `disconnect` listener while a connect attempt is in flight. The transport closes during a failing handshake and dispatches `disconnect` before the catch block sets `errorMessage`; without this guard InspectorView would map the card to plain "disconnected" and the InlineError would never render. - InlineError: opt-in `autoDismissMs` prop that runs a Mantine `slide-up` Transition exit animation after the message has been visible for the given duration. Visibility is derived from a "dismissed message" state (rather than a mounted boolean + setMounted-in-effect), so the timer re-arms whenever `error.message` changes without tripping the react-hooks/set-state-in-effect rule. ServerCard passes autoDismissMs={5000}. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(server-card): own InlineError visibility + animate parent-driven removal Move the 5s auto-dismiss timer and visibility state out of InlineError and into ServerCard so the slide-up exit animation runs in both cases: - Timer-driven: after 5s the alert animates out (existing behavior). - Parent-driven: when `connection.error` is cleared mid-flight (e.g. a successful reconnect), the alert ALSO slides up instead of vanishing. To keep the message painted during the parent-driven exit, ServerCard tracks a `lastError` state that mirrors `connection.error` only on its truthy edges. The conditional setState during render is the supported React pattern for "remember the last seen value of a prop"; gated on message inequality so it can't loop. The `useRef` version of this pattern failed `react-hooks/refs` (no ref writes/reads during render). InlineError is now a pure presentational component: `mounted` prop controls a Mantine `slide-up` Transition. The internal `autoDismissMs` prop is gone — its single caller has moved to the controlled `mounted` pattern. Closes the "error vanishes the moment the parent removes it" half of #1364 (the timer half landed in 6d147e0f). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(server-card): surface connection errors as a toast, drop InlineError Move handshake-failure messaging out of the ServerCard body and onto a Mantine toast (`notifications.show`). The inline alert had a 5-second visibility contract entangled with InspectorView's status pipeline, and the active card was flipping through error → disconnected → error during a failing handshake; surfacing the error as a toast sidesteps that whole timing problem and keeps the card focused on the live status indicator. - App.tsx: toast on the connect-catch path; drop `errorMessage` state + `connectInFlightRef` (the latter was only there to keep the inline alert visible past the transport's close event). - InspectorView: drop the `errorMessage` prop and the `connectionStatus === "error" && errorMessage` mapping branch. - ServerCard: drop the InlineError rendering + the displayedError / errorVisible visibility state machine. - Delete the InlineError component — no other consumers. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(notifications): anchor toasts to the bottom right Move the Notifications portal from top-right to bottom-right so a connection-failure toast doesn't overlap the ViewHeader / tabs row. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * review: address PR feedback on the Connection Info modal 1. Hoist `EMPTY_CLIENT_CAPABILITIES = Object.freeze({})` to module scope in `useInspectorClient` so the fallback returns a stable reference (avoids invalidating downstream `useMemo`/`useEffect` deps when no client is attached). 4. Drop the `?? "stdio"` silent fallback on `connectionInfoTransport` in the JSX — gate the modal render on `activeServer` too. The "stdio" default still lives at the derivation site (config.type is schema-optional and `ServerType` is required by the modal prop), but the gate makes the invariant explicit. 6. Replace the raw `document.querySelector("button.mantine-CloseButton-root")` in `ConnectionInfoModal.test.tsx` with `screen.getByRole("button", { name: /close/i })`. Add `aria-label="Close"` to the CloseButton so the assertion (and screen readers) have a name to match on. 7. Switch the OAuth scope splitter from `/\s+/` to the literal `" "` per OAuth 2.1 §3.3. Also clarify in comments: 2. Note that `connectionInfoOAuth` uses snapshot-at-open semantics on purpose — token refreshes mid-session won't re-render the modal. 5. Note that we prefer the AS-advertised `authorization_endpoint` over `authorizationUrl` because the latter is per-flight noise (state, code_challenge) rather than a stable identifier. Finding 3 (modal exit animation short-circuited by the `{initializeResult && …}` guard) left as intentional — we want the snap-shut on disconnect so a stale info dialog can't keep rendering with no live client behind it. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(connection-info): tighten the OAuth memo comment per re-review The reviewer noted that "snapshot-at-open" was imprecise — the memo re-runs on any of its deps (connectionStatus, inspectorClient, activeServer), which includes settings edits that re-reference the active server. Reframe as "snapshot-at-last-derivation" and call out the natural triggers (server switch, reconnect, settings edit) so a future reader doesn't conclude the memo is open-only. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
dd0f5de820 |
feat(sort): add SortToggle for Logs, History, and Network (#1371) (#1372)
* feat(sort): add SortToggle for Logs, History, and Network (#1371) Introduces a reusable SortToggle element and wires it into the Logs, History, and Network screens so users can flip chronological order. Selection is persisted per-screen to localStorage under the inspector.sortDirection.* namespace, with invalid stored values falling back to the newest-first default. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(logs): move auto-scroll checkbox left of the sort selector Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(logs): remove auto-scroll checkbox; ListToggle left of SortToggle - Drops the Logs auto-scroll checkbox and its plumbing through LoggingScreen / InspectorView. - Moves the ListToggle to the left of the SortToggle on the History and Network panel toolbars. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(sort): address review on #1372 - Hydrate sort direction synchronously (getInitialValueInEffect: false) to remove the one-frame newest-first flicker on first paint. - Drop the redundant `[...matched].sort()` copy across the three panels; `.filter()` already returns a fresh array. - Document why serializeSortDirection bypasses Mantine's default JSON stringify (raw enum value in storage). - MemoryStorage.getItem: collapse `has` + `!` to `?? null`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(history): default the request list to collapsed for parity with Network The History list opened expanded while Network opened collapsed; align History with Network so both panels start in the same compact view. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(history,network): persist list compact state to localStorage Lifts the per-panel `compact` flag out of HistoryListPanel / NetworkStreamPanel and into InspectorView's localStorage hooks, mirroring the sort-direction shape: inspector.listCompact.history inspector.listCompact.network Defaults to collapsed (compact=true) on both, with a corrupted-value deserializer that clamps unknown strings back to the default. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(servers,resources): persist ListToggle state to localStorage Lifts the per-screen compact preference out of ServerListScreen and ResourceControls into InspectorView's localStorage hooks, matching the History/Network shape: inspector.listCompact.servers inspector.listCompact.resources Servers and Resources both default to expanded (compact=false). For ResourceControls the displayed compact is still derived from openSections (so per-section accordion clicks still work locally), and only explicit ListToggle clicks update the persisted preference — the handler computes the next compact value from the click outcome rather than blindly flipping, so a half-open accordion still records the right post-state. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(sort-toggle): switch to icon button matching ListToggle style Replaces the Mantine Select with a subtle Button (or ActionIcon for the subtle variant), using TbSortDescending2 for newest-first and TbSortAscending2 for oldest-first. Clicking the button flips between the two values. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(sort-toggle): restore Select with sort icon in rightSection Reverts SortToggle to a Mantine Select with simplified labels ("Newest First" / "Oldest First") and the corresponding TbSortDescending2 / TbSortAscending2 icon in the rightSection, replacing the default chevron. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(sort-toggle): swap icons so they match label direction Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(sort-toggle): use down arrow for newest, up arrow for oldest Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(inspector-view): extract useSortDirection / useListCompact Collapses seven near-identical useLocalStorage call sites in InspectorView into two scoped helpers. Each helper carries the shared namespace, serialize/deserialize adapters, and getInitialValueInEffect: false; the call sites now read as "this screen has sort persisted to this scope" rather than re-stating the storage shape every time. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(sort-toggle): apply prettier formatting Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
a7178280f0 |
feat(export): wire History + Logs export JSON handlers (#1354) (#1367)
* feat(export): wire History + Logs export JSON handlers (#1354) Replaces the `todoNoop` placeholders in App.tsx with real handlers that serialize the current entries and trigger a browser download via the existing `downloadJsonFile` helper. - Adds `buildExportFilename(kind, serverId, now?)` next to the download helper. Produces sortable, filesystem-safe names like `inspector-history-alpha-2026-03-17T10-00-42.123Z.json` (URL-encodes server ids that contain slashes / spaces / colons, swaps `:` for `-` in the ISO timestamp so the file is safe on Windows). - Wires `onExportHistory` (full MessageEntry list) and `onExportLogs` (the LogEntryData list the screen renders). - Routes the existing Network export through the same filename helper for consistency. Unit tests cover the filename builder, including the no-server-id fallback, special-character encoding, and the default-`now` shape. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(export): tighten buildExportFilename signature + docs - Introduce an `ExportKind = "history" | "logs" | "network"` union and use it as the type of the `kind` parameter. Typos like "hisotry" now fail at compile time and the supported export kinds are documented inline. - Clarify the JSDoc: the segment is omitted when `serverId` is falsy (undefined OR empty string), matching the existing `serverId ? ... : undefined` check. New test pins the empty-string branch. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(logging): remove Copy All button from LogStreamPanel Export now produces the same JSON the Copy All button used to dump to the clipboard, so the button is redundant. History and Network panels have no equivalent — dropping Copy All also brings Logs into parity. Removes the prop from LogStreamPanel + LoggingScreen + InspectorView + App.tsx, and updates the matching tests/stories. Tests now assert the button is *not* in the document so a future re-introduction would have to update the assertion deliberately. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
4641339cc3 |
feat(network): add Network screen surfacing backend fetch log (#1355) (#1363)
* feat(network): add Network screen surfacing backend fetch log (#1355) Routes `FetchRequestLogState` to a new Network tab so users can inspect HTTP-layer request/response headers, status, duration, and category (auth vs transport) of every fetch the backend makes for an active session. Adds `NetworkControls` + `NetworkStreamPanel` + `NetworkEntry` groups, a `monoBreak` Text theme variant for long header values, and wires onClearNetwork / onExportNetwork (JSON download) from `App.tsx`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(network): include request / response bodies in search filter Headers were already covered but bodies were not, so searches for strings that live inside the JSON-RPC payload (e.g. "jsonrpc", "initialize", a tool name) silently returned no matches. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(network): always render Response Body section, note streaming Streaming responses (text/event-stream, application/x-ndjson, POST to /mcp) are intentionally not body-captured by the fetch tracker, so the section was silently omitted. Now whenever a response was received we render the section, with either the body, a "(empty)" note, or a "Streaming response — body not captured" note keyed off content-type. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(network): capture response bodies via async body-update events Two related fixes: 1. The fetch tracker was marking every streamable-HTTP POST /mcp call as "not captured" because the response content-type is text/event-stream. In practice those streams are bounded — the server sends the JSON-RPC reply (sometimes preceded by progress events) and closes — so we can clone the body and read it. Only GET + SSE / ndjson is the unbounded long-lived channel that has to stay uncaptured. 2. Reading the body inline blocked the transport from processing progress notifications until the entire stream finished, breaking the `resetTimeoutOnProgress` integration test. The body is now read in the background and dispatched via a new `fetchRequestBodyUpdate` event that the FetchRequestLogState patches into the matching entry by id. Plumbing wires through node/transport, the remote SSE channel (RemoteSession, RemoteClientTransport), and the remote event types. Also hides the Network tab when the active server is stdio (no HTTP traffic to surface) and bumps the inline body preview limit to 100 KB so typical tools/list responses render rather than tripping the "too large" notice. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(network): show orange SSE badge for long-lived stream entries Adds a small "SSE" badge to the right side of the NetworkEntry header (left of the HTTP status badge) when the response is a long-lived streaming channel (GET + text/event-stream or application/x-ndjson) — the only case where the body genuinely isn't captured. Makes the "not captured" placeholder less surprising at a glance. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(network): address PR review findings - Stop capturing OAuth response bodies (drop updateResponseBody from the effectiveAuthFetch tracker). Restores pre-PR behavior for auth-category entries so live access_token / refresh_token values can't leak into the Network tab body preview during a screen-share. Headers + status are still tracked. - Extract `isLongLivedStreamResponse(method, contentType)` to core/mcp/fetchTracking so the rule lives in one place; consumed by the tracker and the NetworkEntry UI placeholder. - Document the intent of NetworkEntry's `useEffect` (the list-level toggle is authoritative; per-entry overrides are intentionally discarded on each list toggle). - Rename MAX_INLINE_BODY_BYTES → MAX_INLINE_BODY_CHARS (`.length` is UTF-16 code units, not bytes) and update the user-facing notice. - Switch NetworkStreamPanel's search from joined-string `.includes` to per-field `.some(.includes)` so a term can't span field boundaries. Regression test added. - Add aria-label to ListToggle ("Expand all" / "Collapse all") — both an a11y improvement and a stable selector that lets the panel test stop fishing the button out by text-content negation. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(network): use ListToggle aria-label in panel + modal tests Cleanup spotted in PR review follow-up: - HistoryListPanel + ServerSettingsModal previously fished the ListToggle out by empty text content because the toggle had no accessible name. Now that ListToggle exposes "Expand all" / "Collapse all", both tests can use getByRole + name and the stale comments go away. - ListToggle's own tests now assert the aria-label contract so a future refactor can't silently drop it. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(network): expose category toggle state via aria-pressed The category UnstyledButtons in NetworkControls previously communicated their toggled state only via background color. Adds aria-pressed={active} so assistive tech can discover the on/off state without visuals, with a test that asserts the attribute tracks the visibleCategories prop. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
491a522e8d |
fix(stories): wait on dialog role with longer timeout for ServerConfigModal plays
The four ServerConfigModal play functions used `findByText("Add server")`
etc. to assert the modal mounted. Mantine's Modal portals to document.body
outside the storybook canvas, and its first paint can race against the
1000ms default that `findBy*` queries retry against — especially in the
Storybook dev UI (slower than `test:storybook`'s headless browser,
particularly with devtools open or coverage instrumentation on). When the
play fired before the modal mounted, findByText timed out, Storybook
flagged the story as FAIL, and left the "preparing" loader visible.
Switch to `findByRole('dialog', { name })` with a 5s timeout, and scope
subsequent queries (`getByLabelText`, etc.) to the dialog's subtree via
`within(dialog)`. The role-based query asserts the dialog actually mounted
(more semantically meaningful than a free text lookup), the longer ceiling
covers slow first-paint, and scoping to the dialog avoids false-positive
matches from sibling loader overlays.
Verified locally: 1758 tests pass, 8/8 consecutive reloads of the Add Empty
story in the Storybook dev UI now show "Pass".
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
f6ac075f24 |
fix(servers): make settings form re-render on every keystroke (#1361) (#1362)
* fix(servers): make settings form re-render on every keystroke (#1361) The settings modal is fully controlled by App.tsx, but pending edits were stashed in a useRef — refs don't trigger re-renders, so the input's `value` prop stayed stale until the 300ms debounce + PUT + refetch round-tripped. Clicking "Add Header" / "Add Metadata" showed no row, and typing in OAuth fields had no character echo, until that round-trip completed (~500ms). The bug landed in #1353 with the debounced-flush pattern. It surfaced after #1356 because per-keystroke PUTs now also write the keychain, making the round-trip latency more noticeable. Lift the draft into `useState`. Every change re-renders the modal immediately. Keep the 300ms debounce on the PUT side. Initialize the draft from the server-list entry only when the modal opens to a new target (not on every `servers` change) so background refreshes don't clobber the user's in-progress edits. Flush on close to preserve final keystrokes. Verified live against the dev server: header/metadata rows appear on click, OAuth Client ID / Client Secret typing echoes without lag, and the values persist correctly (clientId in mcp.json, clientSecret in the macOS Keychain). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(servers): extract useSettingsDraft hook + tests; review polish (#1361) Picks up the three review observations on #1362: 1. Extract the draft state + debounce + flush logic from App.tsx into `core/react/useSettingsDraft`. Same behavior; thinner App.tsx; testable in isolation with `renderHook`. Tests pin the regression #1361 was about (synchronous onChange → draft update) plus the debounce window, burst-collapse, flush-on-close, error propagation, and the "background refresh must not clobber in-progress edits" contract via a `resolveInitial` closure swap. 2. Hoist the empty `InspectorServerSettings` fallback to a module-scope `EMPTY_SETTINGS` const. Both the hook's init path and the modal's closed-state fallback share the same object identity; the inline `useMemo` that previously re-derived it is gone. 3. Add a comment on the `resolveInitial` callback noting that we read `servers` from this render's closure under the assumption that the entry point is the "Settings" button on a rendered server card. Documents the contract for future callers (e.g. a keyboard shortcut that opens the modal pre-hydration). Validate clean: 1756 tests pass. New useSettingsDraft.test.tsx adds 13 test cases. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(servers): stabilize flush identity + add unmount-cleanup test (#1361) Two non-blocking observations from the third review on #1362: 1. `flush` identity churned on every keystroke because the useCallback closed over `draft` + `targetId`. Read both through refs instead so the returned callback identity is stable. The behavior is unchanged — but the consumer's `onSettingsModalClose` (which wraps `flush`) no longer re-allocates per keystroke, which means the modal's `onClose` prop identity also stays stable. A new test pins this. 2. Added a comment in `onChange` calling out that callers switching `targetId` mid-debounce must `flush()` first to preserve the pending PUT. The only switch path today (`onSettingsModalClose` in App.tsx) does that already — the comment is for future refactors. 3. New unmount-cleanup test pins the intentional "drop final <debounceMs window on unmount" trade-off — flushing on unmount could fire a PUT against an unmounting component, which is a worse footgun than losing a few hundred ms of typing on route change / HMR. Validate clean: 144 files, 1758 tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
037225a9fb |
feat(servers): move oauthClientSecret and stdio env values to OS keychain (#1356) (#1360)
* feat(servers): move oauthClientSecret and stdio env values to OS keychain (#1356) mcp.json is designed to be tool-shareable (symlinked from Claude Desktop's config, pasted into bug reports, synced via dotfiles), so storing OAuth client secrets and stdio env values in plaintext there meant any of those flows could leak them. Lift both into the OS keychain via @napi-rs/keyring (active replacement for the archived keytar). The wire shape is unchanged: the GET /api/servers handler rehydrates from the keychain so browser code sees the same JSON; the on-disk file no longer contains the secret material. Includes an idempotent migration that lifts plaintext from older mcp.json files (or hand-edited ones) into the keychain on first read. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(servers): tolerate keychain unavailability on non-set ops (#1356) CI on Linux failed because `@napi-rs/keyring` hard-errors without libsecret, and the route handlers were touching the keychain even for no-secret flows (defensive `deleteAllForServer` on POST/DELETE, sweep on PUT). The user's intent for the Linux fallback was "hard fail when a secret is actually involved" — not on every keychain touch. Make `KeyringSecretStore.get / delete / deleteAllForServer` silently degrade when the keychain is unavailable (return null / no-op); `set` remains the one operation that hard-fails with `KeychainUnavailableError`. The migration path catches the error and preserves the disk plaintext rather than losing the secret. Also inject `InMemorySecretStore` in `useServers.test.tsx` and `servers-events.test.ts` so tests never touch a developer's real keychain even when libsecret is present. Adds `KeyringSecretStore` tests (vi.mock'd native bindings) and an integration suite that simulates Linux-without-libsecret to verify the tolerance contract end-to-end. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(servers): close GET migration race; reverse keychain/disk order (#1356) Addresses code review: 1. **Concurrency**: GET /api/servers was reading + migrating outside the write lock, then writing inside it — a concurrent POST/PUT/DELETE between the read and the lock acquisition could be clobbered by the migration write. Also affected the first-launch seed write. Fix: take a fast unlocked peek; if the file is absent (seed branch) or carries plaintext to migrate (migration branch), re-read inside the write lock and decide there. Added `hasPlaintextSecrets()` as a pure predicate so the common no-migration path stays lock-free. 2. **Write ordering**: POST and PUT wrote disk first, then keychain. A keychain `set` failure mid-write left a half-configured entry on disk and trapped retries at 409. Reversed to keychain-first so a 503 leaves both stores in their pre-write state. PUT in-place now sets new fields, writes disk, then deletes obsolete fields (a failed disk write leaves orphan keychain entries — recoverable — rather than premature deletions of fields the user still expects to see). 3. **Tolerance simplification**: dropped the no-entry regex in `KeyringSecretStore.delete` — both "no entry" and "keychain unavailable" collapse to the same desired outcome (the entry isn't there anymore), so a uniform swallow is clearer. 4. **Parallelism**: `readKeychainEntriesFor` now `Promise.all`s the per-field `get` calls. macOS Keychain round-trips are 10-50ms; serializing 5+ env vars per server × N servers is a meaningful wall-clock cost on GET. 5. **Spec doc**: reflects the tolerance contract (only `set` hard-fails) and the keychain-before-disk write ordering rationale. New regression test in servers-route.test.ts covers the retry-after-503 contract directly. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(servers): parallel keychain writes + migration partial-state comment (#1356) Two nice-to-have observations from the re-review: - `writeKeychainEntriesFor` now `Promise.all`s its per-field sets, matching the read side. No ordering requirement among distinct (id, field) keys; on unavailability every set rejects and Promise.all surfaces the first rejection (still 503 from the route handler). - `migratePlaintextSecrets` catch block now spells out the partial-migration semantics: we return the original config so the disk plaintext stays intact, and the next successful GET retries with the keychain-wins branch absorbing already-set entries. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(servers): parallel keychain deletes for symmetry (#1356) Picks up the symmetry observation from the third review: `deleteKeychainFields` now uses `Promise.all` like its read and write siblings. Distinct (id, field) deletes have no ordering requirement, and `secretStore.delete` is already a silent no-op on unavailability so Promise.all has no failure-mode surprise. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
a9df501856 |
feat(servers): flatten mcp.json settings to top-level fields (#1358) (#1359)
* feat(servers): flatten mcp.json settings to top-level fields (#1358) Eliminate the nested `settings` block introduced in #1352 so each server entry's Inspector-extension fields live as direct keys alongside `type` / `url`, matching the `.mcp.json` shape Claude Code / Cursor / Cline write: - `headers` becomes a `Record<string, string>` on disk (was nested `settings.headers` as a pair-array) - `oauth` becomes a nested `{ clientId?, clientSecret?, scopes? }` object (was flat `settings.oauthClientId` / `oauthClientSecret` / `oauthScopes`) - `metadata`, `connectionTimeout`, `requestTimeout` move to the entry's top level as-is (Inspector-only fields) The in-memory + wire shape is unchanged from #1352: `InspectorServerSettings` keeps pair-array `headers` and flat `oauth*` fields because the form needs them in that shape to drive controlled-component editing. The bidirectional conversion lives in `serverList.ts` (`storedFieldsToInspectorSettings` / `inspectorSettingsToStoredFields`) and is invoked by `mcpConfigToServerEntries` / `serverEntriesToMcpConfig` and by the server route's `buildStoredEntry`. The `PUT /api/servers/:id` wire envelope keeps the preserve/clear/apply semantics from #1353 — the backend splats validated `settings.*` onto top-level disk keys when assembling the next on-disk shape. Hard-cutover migration per the issue's decision 4: files written by the one pre-#1358 v2/main build have a nested `settings` block; we drop the node on read with a one-line warn including the server id. The persisted headers / metadata / timeouts / OAuth credentials are intentionally lost on first read — users re-enter them via the form (or hand-edit the file into the flat shape). v2 has not shipped a stable release with the nested shape, so the blast radius is dogfooders only. The smuggle-guard in `buildStoredEntry` widens from just `settings` to all six Inspector-extension keys (`settings`, `headers`, `metadata`, `connectionTimeout`, `requestTimeout`, `oauth`), so a caller can't bypass `validateSettings` by nesting any of them inside `config`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(servers): address PR review on flatten (#1359) - Single-source the Inspector-extension key list with `satisfies` (finding 1). `INSPECTOR_FIELD_KEY_MAP satisfies Record<keyof StoredInspectorFields, true>` in serverList.ts means a new field on the type forces a compile error here. The set is exported and re-used by the server route's smuggle guard, so adding `proxy` or similar in the future updates all three sites in lockstep. The PUT preserve path now uses a new `stripInspectorFields` helper that derives from the same source instead of hand-destructuring. - Add read-path shape validation in `normalizeMcpServers` (finding 2): `headers: "oops"` / `metadata: 42` / `oauth: []` / non-numeric timeouts are dropped individually with a warn, so a hand-edited malformed file can't put garbage rows into the form. Mirrors the write-side `validateSettings` symmetry. New integration test covers each malformed shape with a partial fixture so a single bad key doesn't take out the whole entry. - Align new warns to the `fileLogger || console.warn` fallback pattern (finding 3) via a small `logWarn` helper, so the legacy-drop and smuggle warns are visible in `npm run dev` where the logger is often unconfigured. - Spec doc tweak: the CLI/TUI out-of-scope note's "writes to `settings.headers`" reference now reads "writes to the entry's top-level `headers` field on disk (post-#1358 flat shape)" (finding 4). Avoids confusion for future readers. - Nits: replace `current.mcpServers[originalId]!` with explicit narrowing; add a comment on the OAuth truthiness drop in `storedFieldsToInspectorSettings` mirroring the write-side comment. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(servers): address pass-2 review on flatten (#1359) - Tighten `isNonNegNumber` with `Number.isFinite` so `Infinity` and `NaN` are rejected at the read boundary, matching the write-side intent (finding D). Hand-edited `connectionTimeout: Infinity` would have passed the prior check and propagated to the form. - Add a one-line comment in `normalizeMcpServers` noting that the per-entry object is mutated in place via the `delete` calls — safe for today's callers (freshly parsed JSON, no other reference held) but worth flagging for the next reader (finding A). Skipping findings B (move type guards to module scope — single call site) and C (`isOauthObject` lenient on extra keys — reviewer confirmed this is the intended forward-compat behavior, matching `normalizeServerType`'s unknown-key passthrough). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(servers): fix mutation-comment caller branch label (#1359) The previous comment named the GET handler's "seed branch" as a caller of normalizeMcpServers, but that branch returns DEFAULT_SEED_CONFIG directly without normalizing. The actual second caller is the file-present branch (after parseStore). Correcting the label so a future reader looking for the seed branch caller doesn't waste time. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
5f9bfdc490 |
feat(servers): hot-reload server list on external mcp.json edits (#1345) (#1357)
* feat(servers): hot-reload server list on external mcp.json edits (#1345) Watch ~/.mcp-inspector/mcp.json from the backend and push a change event on a new /api/servers/events SSE channel. The web client subscribes via fetch + ReadableStream (so the existing bearer-auth contract is preserved — EventSource can't send custom headers) and re-fetches on each event. The watcher is lazy: it starts on the first SSE subscriber and stops when the last one disconnects, so tests that never open the channel don't spin up a real fs watcher. Self-fires from the backend's own POST/PUT/DELETE are suppressed by capturing the post-write mtime and comparing it on each watcher event. Chokidar is used for cross-platform editor-save semantics (temp-file + rename produces unlink+add sequences that bare fs.watch handles poorly on macOS/Linux); awaitWriteFinish coalesces those into a single event. createRemoteApp now returns a close() to release the watcher, which the standalone server and the vite dev plugin chain into their existing close paths. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(servers): address PR review on hot-reload (#1357) - writeMcpAndTrackMtime now stat()s the file before writing; if the mtime differs from the last value we recorded, an external editor slipped in between our writes and chokidar would coalesce both into one event whose mtime matches our own write — broadcast directly so peer subscribers learn about the external edit (review finding 1, 5). - Split useServers.refresh into a public refresh() (loading toggle) and an internal refreshInternal(background) the SSE handler calls; the list now stays visible during background refreshes instead of flashing skeleton state on every external mcp.json edit (finding 2). - Coalesce multiple \n\n frames landing in one decode chunk to a single refresh call so back-to-back broadcasts don't race two concurrent GETs whose setState order is unspecified (finding 3). - Clarify createRemoteApp's close() docblock about in-flight SSE stream callbacks (finding 7). - Add integration coverage for PUT/DELETE self-suppression and for the external-edit-between-backend-writes broadcast (finding 6). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(servers): address pass-2 review on hot-reload (#1357) - Clarify writeMcpAndTrackMtime comment so a future reader can't mistake the broadcast for a content-preservation guarantee: the external edit's content may already have been read-around and overwritten depending on the interleave with readMcpConfig. - Drop the no-op GET in the PUT/DELETE self-suppression tests. The pre-stat branch is short-circuited by `lastWrittenMtimeMs === null` with or without it; matches the POST test's structure. - Add explicit no-flicker regression test: tracks every `loading` value across renders during an SSE-driven refresh and asserts none flipped back to true. Catches a future regression that reaches for setLoading(true) in the background path even under React batching. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
85a4b4c057 |
feat(servers): persist per-server settings to mcp.json (#1352) (#1353)
* feat(servers): persist per-server settings to mcp.json (#1352) - Add optional `settings` node alongside the transport config in each `~/.mcp-inspector/mcp.json` entry. Round-trips through `mcpConfigToServerEntries` / `serverEntriesToMcpConfig` and the `/api/servers` POST/PUT routes. - Drop `MCPServerConfig.headers` from `SseServerConfig` / `StreamableHttpServerConfig` and remove the headers textarea from `ServerConfigModal`. Custom headers are now entered only in `ServerSettingsForm` and reach the wire via `settings.headers`. - Wire `ServerSettingsModal` into `App.tsx`; saves dispatch through a new `useServers.updateServerSettings` mutator. `updateServer` carries the existing settings across config-only saves so they aren't dropped. - Apply settings on the *first* outbound request after a server loads — no need to open the settings form first: - `settings.headers` → SSE / streamable-http wire headers. - `settings.metadata` → `InspectorClientOptions.defaultMetadata`, merged into every outgoing `_meta` (call-time keys win). - `settings.requestTimeout` → `InspectorClientOptions.timeout`. - `settings.connectionTimeout` → Promise.race wrapper around connect(). - `settings.oauth*` → pre-seeded OAuth client credentials. - `RemoteConnectRequest` carries `settings` so the remote backend can source transport headers from the same source as the local path. - `oauthClientSecret` is persisted in `mcp.json` alongside stdio `env` values, both protected by the file's `0o600` permission. - Update `specification/v2_servers_file.md` with the new shape and the removal of the legacy headers field. Tests: round-trip in `serverList.test.ts`; persistence on POST/PUT in `servers-route.test.ts`; first-connect header injection in `transport.test.ts`; `defaultMetadata` merging in `inspectorClient.test.ts`; `useServers.updateServerSettings` + `updateServer` settings-preservation in `useServers.test.tsx`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(servers): rebuild InspectorClient on every connect so settings refresh The previous onToggleConnection branch reused the existing InspectorClient on a same-server reconnect, which froze settings at the moment the client was first constructed. Editing headers, metadata, timeouts, or OAuth credentials in the settings form was therefore invisible until the user either switched servers or reloaded the page — surprising right after a visible save. Always call setupClientForServer on (re)connect so the latest target.settings flow through to the transport's headers / requestInit, to defaultMetadata, and to the OAuth manager. Also adds remoteClientTransport tests that pin the wire-level contract: settings round-trip through the /api/mcp/connect body when supplied, and the field is omitted when no settings are configured. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(history): stop wiping pendingRequestEntries inside the connect event State managers (ManagedToolsState, ManagedPromptsState, ManagedResourcesState, ManagedResourceTemplatesState, ManagedRequestorTasksState) handle the client's "connect" event by calling `void this.refresh()`. The synchronous portion of refresh runs through SDK Protocol.request → transport.send → MessageTrackingTransport.send → trackRequest, all before yielding to the network — so each list request gets tracked into MessageLogState's pendingRequestEntries inside the same connect event dispatch. MessageLogState's own "connect" listener was registered after those state managers (it's the last one constructed in App.tsx) and cleared pendingRequestEntries on every connect. Listener order meant the clear ran *after* the requests were tracked but before their responses arrived, leaving the history with unmatched "response" entries marked PENDING. The clear-on-disconnect (via statusChange → disconnected) already handles session-boundary cleanup, so clear-on-connect was purely defensive and now actively harmful. Drop the connect listener; keep the disconnect path. Tests: updated the unit test to assert the new behavior (connect does not clear); added an integration regression that wires a sibling connect listener firing listTools and asserts the response folds into the request entry instead of appearing as an orphan. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore: drop accidentally tracked debugging screenshot pending-requests.png was a local debugging artifact attached to a conversation, not source. Remove it from the tree (the working copy is kept so the user still has it locally) and rely on the existing untracked behavior going forward. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(servers): add e2e check that settings.headers reach upstream MCP Stitches the full v2 pipeline together in one assertion: browser-style RemoteClientTransport → POST /api/mcp/connect (settings in body) → backend createTransportNode applies settings.headers to requestInit → SDK StreamableHTTPClientTransport → upstream MCP test server Captures the upstream HTTP request via FetchRequestLogState and asserts both `X-Tenant` and `X-Trace` are present on the outbound POST. Catches any regression that breaks the wire between the form and the upstream server in a single integration test, instead of the previous coverage which exercised each hop in isolation. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(ServerSettingsModal): invert ListToggle compact prop so icon matches state ListToggle's `compact` prop represents the list's *current* compact state: true means "currently compact" and renders the expand icon to advertise the action (open up). The settings modal was passing `compact={allExpanded}` — exactly inverted. When all sections were open it showed the expand icon (suggesting "open more") and when collapsed it showed the collapse icon, opposite of every other ListToggle caller in the app. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * style(HistoryListPanel): match ServerListScreen Export button — rename + default variant Renames "Export JSON" to "Export" and switches from the subtle ToolbarButton to the same `Button variant="default"` used by ServerListControls so the two Export affordances look identical across screens. Also moves the button to the right of the ListToggle (mirroring the order on the server list screen) and disables it when there are no entries to export. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * style(HistoryListPanel): move Clear button to top toolbar, match Export style Moves the Clear affordance out of the "History (N)" section header and up to the panel toolbar alongside Export, where it now uses the same `Button variant="default"` styling. Order in the toolbar is now: ListToggle · Clear · Export. Clear is disabled when there are no unpinned entries to remove (pinned entries are intentionally preserved). ToolbarButton helper is unused after this change and dropped. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore: ignore local debugging screenshots at repo root Stops `git add -A` from re-staging the `*.png` files Claude Code drops at the repo root from attached conversation screenshots. The pattern only matches the top-level (`/*.png`) so legitimate image assets in subdirectories (e.g. `clients/web/public/`) still get tracked when explicitly added. Also untracks the two screenshots that slipped through previously (initialize-request.png, pending-requests.png). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * style(LogStreamPanel): match HistoryListPanel button styling Clear / Export / Copy All on the Logging screen were rendered with the subtle ToolbarButton helper while History's equivalents use `Button variant="default"`. Switch all three to the default variant so the toolbars look identical across screens and mirror the disabling behavior — each button is now disabled when `entries.length === 0`. ToolbarButton helper is unused after this change and dropped. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(servers): address PR review — debounce save, preserve-on-omit PUT, validate shape, lift connect timeout Round-up of fixes from https://github.com/modelcontextprotocol/inspector/pull/1353#issuecomment-4526861959: 1. ServerSettingsModal saves were firing on every keystroke (one PUT + full mcp.json refresh per character). App.tsx now debounces by 300ms and flushes on modal close so a burst of edits coalesces into one PUT and the form value can't flicker back mid-typing. 2. `PUT /api/servers/:id` with an omitted `settings` field used to silently wipe the persisted settings node. The route now treats the three intents distinctly: - field omitted → preserve existing on-disk settings - explicit null → clear the settings node - settings object → validate + apply `useServers.updateServer` is simplified to drop the client-side carry-through hack, and the test that documented the broken behavior is flipped + augmented with explicit-clear and bad-shape cases. 3. `connectionTimeout` Promise.race is moved from App.tsx into `InspectorClient.connect()`, reading `this.serverSettings?.connectionTimeout`. TUI/CLI consumers now get the same behavior; on timeout the client internally disconnects so the next connect starts clean. 4. Custom Headers hint in `ServerSettingsForm` notes that `Authorization` is owned by the OAuth flow when OAuth is configured — avoids the sharp edge where the SDK's authProvider silently overwrites a user-set Authorization header. 5. New `validateSettings` on the server route rejects malformed shapes (`settings: []`, `settings.headers: "oops"`, non-numeric timeouts, etc.) with 400 instead of letting them persist and crash the UI. 7 (nit). Renamed `settings` → `savedSettings` inside `setupClientForServer` so the variable holding the persisted settings doesn't shadow the later locally-derived values. Follow-ups opened separately: #1356 — Move oauthClientSecret out of mcp.json into the OS keychain (review #6). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(servers): address PR review pass 2 — connect-timeout test, unmount cleanup, tagged intent, lenient settings read Pass-2 review at https://github.com/modelcontextprotocol/inspector/pull/1353#issuecomment-4527089705: (a) New integration test pins the new client-layer connect-timeout contract: a hanging stub transport with `connectionTimeout: 50` rejects with the descriptive error and lands the client in "error" status. Documents what TUI/CLI consumers should expect from the lifted Promise.race in InspectorClient.connect(). (b) useEffect cleanup clears `pendingSettingsTimerRef` on unmount so a route change or HMR mid-debounce can't fire one stale PUT against an unmounted component. (c) Refactor the PUT-route `settingsIntent` discriminator from `"preserve" | "clear" | { value }` to a tagged union with `kind`, and consume it with a `switch` for exhaustiveness — reads cleaner and makes adding a fourth intent painless. (d) `normalizeMcpServers` now runs `validateSettings` on the on-disk settings node and silently strips it if invalid, mirroring the existing lenient-on-read pattern from `normalizeServerType`. Closes the read/write asymmetry where a hand-edited malformed `settings` could survive on disk and get carried through preserve-on-PUT. New route test pins the strip-on-GET + no-propagate-on-preserve-PUT behavior. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(servers): pass-3 review — patch-style PUT, toast on close-flush fail Pass-3 review at https://github.com/modelcontextprotocol/inspector/pull/1353#issuecomment-4529276897. (b) `PUT /api/servers/:id` now treats both `config` and `settings` as optional patches: - field omitted → preserve the on-disk value - explicit body value → apply - settings: null → clear `updateServerSettings` stops reading `existing.config` from the in-memory `servers` snapshot, which closes the closure-capture hazard where a refresh between debounce-schedule and flush could silently revert a separate edit. The route is the single source of truth for transport config now. New tests cover settings-only PUT, rejected non-object config, and the empty-body no-op patch case. The previous "rejects a missing config" test is rewritten to document the new no-op-preserve semantics. (c) Inline comment near the PUT rebuild loop documents that writing the full mcpServers map back is what gives the route its desirable self-healing property for malformed settings on *other* entries — deliberate side-effect, not an accident of normalization. (d) `flushPendingSettings` surfaces failures via @mantine/notifications instead of swallowing them. The modal already closed by the time the flush PUT resolves, so a silent fail would leave the user thinking their last edits saved (especially painful for the OAuth client secret). The notifications wiring was already mounted in main.tsx. Pass-3 (a) — settings form being effectively uncontrolled mid-edit — is acknowledged in the reply: working as intended today; the proper fix is internal modal state when file-watching (#1345) forces the issue. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(servers): pass-4 review — strip smuggled settings out of config Pass-4 review at https://github.com/modelcontextprotocol/inspector/pull/1353#issuecomment-4529861349. Main finding: `normalizeServerType` spreads unknown keys, so a body that nested `settings` inside `config` would smuggle a settings field onto the stored entry without ever passing through `validateSettings`. Strip the settings key off the incoming config in `buildStoredEntry` so `validateSettings` remains the single write path for the settings node — the "one source of truth" invariant pass 2 set up holds again. Two new tests pin the strip on both routes: - POST with `config.settings = { bogus }` lands an entry with no settings node on disk. - PUT with `config.settings = { bogus }` + `settings: null` clears the real settings *and* doesn't re-attach the bogus payload via the spread. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(servers): pass-5 review — absorb late connect rejection, tighten validateSettings, log smuggle warning Pass-5 review at https://github.com/modelcontextprotocol/inspector/pull/1353#issuecomment-4529954490. Main finding: when the connect-time `Promise.race` lost to the timeout, the original `this.client.connect(this.transport)` promise was left pending. After `disconnect()` tore the transport down, real transports (SSE / streamable-http) would reject that promise later — with no handler attached, producing a Node `unhandledRejection` (and a vitest suite warning). Attach a no-op `.catch` right after creating the connect promise so Node sees the late rejection as handled. Nit (a): `validateSettings` was casting the raw object through to `InspectorServerSettings`, which let unknown keys ride along onto disk. Replaced the cast with an explicit pick-and-build over the known fields. Unknown stowaways now silently drop on the way through. New route test pins the behavior. Nit (b): `buildStoredEntry` now logs a `warn` via the route's pino logger when an incoming `config` carries a `settings` key. The strip is still defensive (it isn't a documented contract) but the warning lets a misconfigured client find out about the wasted field instead of debugging a silent drop. Nit (c) — the defensive `?? {}` fallback in `buildStoredEntry` — acknowledged in the review reply; left in place as belt-and-braces. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(servers): pass-6 polish — scope absorber, thread id, drop empty OAuth, guard `in` Pass-6 review at https://github.com/modelcontextprotocol/inspector/pull/1353#issuecomment-4529979135. Reviewer signed off as ready; landing the four marginal cleanups since they're all small. (a) `connectPromise.catch(() => {})` moves inside the `if (connectTimeoutMs > 0)` branch. The await-only path propagates rejection cleanly through the outer try/catch — the absorber is only needed when the race leaves the original promise unattended. Code now reads as "absorber is tied to the race," which is the real invariant. (b) Thread `id` (POST id, PUT newId) through `buildStoredEntry` and include it in the smuggle warning's pino bindings. Two clients smuggling simultaneously are distinguishable in the log instead of indistinguishable. (c) `validateSettings` coerces empty-string OAuth fields to absent. The form emits `""` when the user clears an input, and an empty `oauthClientId` on disk would later be misread as "OAuth configured." New route test pins it. (d) `buildStoredEntry` adds a null/object guard before `"settings" in configObj`. The route layer pre-checks this on POST and PUT, but the helper's `unknown` parameter is a weak contract; the guard makes it safe to call from anywhere. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(spec): reflect settings persistence + patch-PUT contract in v2_servers_file The doc was authored when `mcp.json` only stored the server list. Bring it forward to match what shipped in #1352 / PR #1353: - Summary calls out that the file also stores per-server settings and links into the Per-server settings section. - On-disk format note explains that `settings` is an optional Inspector- specific extension that other MCP tools ignore. - New top paragraph on the Per-server settings section explains the basic-config / settings UI split rationale (simpler form for the common case, advanced settings tucked behind a second dialog). - UI bullet is rewritten to match the patch-style PUT semantics that emerged across the review cycle: settings-only and config-only PUTs preserve their counterpart inside the write lock, so neither modal can silently clobber the other half. The earlier "must re-send settings on PUT" hack is gone. - New bullets capture the save cadence (300ms debounce + flush-on-close + failure toast), the patch-PUT contract (omit-preserve / null-clear / rename), and the write-path / read-path validator gates. - Secret storage bullet links to #1356 (OS-keychain follow-up). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(spec): show a third entry with the optional settings node The On-disk format example demonstrated only basic stdio entries. Add a streamable-http entry with a populated `settings` node so readers can see the shape inline rather than having to scroll to the Per-server settings section. Inline comment in the block cross-links there for the full contract and notes that other MCP tools ignore the extension key. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
420da2e415 |
feat(servers): export server list as mcp.json download (#1346) (#1351)
* feat(servers): export server list as mcp.json download (#1346) Adds an Export button to the Servers screen, next to "Add Servers". Clicking it downloads the current server list as a canonical mcp.json file (2-space indent, same shape serializeStore writes), so the user can hand it off, sync it to another machine, or symlink into Claude Desktop / Cursor / Cline. Wiring: - ServerListControls: new Export Button (variant=default), disabled when serverCount === 0 (nothing to download). - ServerListScreen / InspectorView: thread `onExport` / `onServerExport` through. - App.tsx: onServerExport runs serverEntriesToMcpConfig on the in-memory servers list, JSON.stringify with 2-space indent, and triggers a Blob download via a temporary anchor (appended to body for Firefox compatibility, then removed; object URL revoked). Tests: - ServerListControls unit tests cover button-count (toggle hidden vs shown, Export always present), disabled-on-empty, click fires onExport. - ServerListControls.stories grows play functions for both stories — WithServers asserts Export is enabled + click fires the spy in real Chromium, WithoutServers asserts Export is disabled. - Existing screen + view tests / stories updated for the new prop. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(servers): address PR #1351 review (extract helpers + exception safety + guard) Substantive items from the review: 1. Serialization format parity (review #1): added `serializeMcpConfig(entries)` to core/mcp/serverList.ts — a browser-safe helper that runs `serverEntriesToMcpConfig` then stringifies with 2-space indent. Single source of truth for export formatting; can't drift from serializeStore's on-disk shape. 2. Exception safety on the anchor cleanup (review #2) + a direct test for the App.tsx download wiring (review #6): extracted `downloadJsonFile(filename, json)` to clients/web/src/lib/ downloadFile.ts. removeChild + revokeObjectURL run in a `finally` so a thrown click() doesn't leak the DOM node or the object URL. Unit-tested under happy-dom with URL.createObjectURL mocked (happy-dom doesn't ship it). 3. Empty-list guard (review #5): App.tsx onServerExport bails when `servers.length === 0`. The button's disabled prop already prevents the UI path, but the handler is now locally correct against any future programmatic caller (storybook, keyboard shortcut, etc.). Acknowledged but not changed: - Tooltip on the disabled Export button (review #3) — Mantine's disabled-button + Tooltip wart wants a span wrapper + custom events config. Optional per the reviewer; deferred. - Repeated exports → `mcp (1).json` browser suffix (review #4) — a deliberate trade-off since `mcp.json` is the conventional name a user would expect to symlink into Claude Desktop / Cursor / etc. Tests: 1647 unit (+6) + 410 integration + 306 storybook = 2363 green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
db1916cd34 |
feat(servers): persist server list to ~/.mcp-inspector/mcp.json (#1343) (#1349)
* feat(servers): persist server list to ~/.mcp-inspector/mcp.json (#1343) Replaces the hardcoded SEED_SERVERS in App.tsx with a file-backed list at ~/.mcp-inspector/mcp.json. Full CRUD round-trips through new /api/servers routes; first launch writes the two existing seeds if the file is absent. Backend (core): - store-io: getDefaultMcpConfigPath() — sibling of getDefaultStorageDir() - core/mcp/serverList.ts: pure converters MCPConfig <-> ServerEntry[], DEFAULT_SEED_CONFIG, and normalizeServerType (moved from node/config.ts to keep serverList.ts Node-free) - core/mcp/remote/node/server.ts: GET/POST/PUT/:id/DELETE/:id /api/servers routes, plus RemoteServerOptions.mcpConfigPath defaulting to getDefaultMcpConfigPath(). Ids validated with validateStoreId. Frontend: - core/react/useServers.ts: { servers, loading, error, refresh, addServer, updateServer, removeServer } over the new endpoints - App.tsx: drops SEED_SERVERS, wires useServers, disconnects on remove of the active server, follows id on rename of the active server - ServerConfigModal: Add/Edit/Clone form (stdio: command/args/env/cwd; sse/streamable-http: url/headers as one-per-line text) - ServerRemoveConfirmModal: confirm-on-remove with id + transport summary Tests (52 new): - unit: getDefaultMcpConfigPath env-var permutations, serverList round-trip, hook flows against real Hono app.fetch (no TCP), modal forms + validation - integration: /api/servers routes against a real createRemoteApp + tmp file - stories with play functions for both new modals Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(servers): address PR #1349 review (correctness + nits) Bugs: - normalizeServerType: validate against the allowed set and default unknown / non-string types to "stdio" (matches the missing-type case). Prevents a hand-edited {"type":"websocket"} from leaking past `as ServerType` and surprising downstream narrowing. - onConfirmRemove: when removing the active server, also destroy() the 9 state managers and null them + the client out so they GC immediately instead of waiting for the next server switch. - /api/servers mutating routes: serialize POST/PUT/DELETE through an in-process write-lock so concurrent read-modify-write cycles can't lose updates. New integration test fires 25 parallel POSTs and asserts every entry lands. - readMcpConfig comment: update to match actual behavior (valid-JSON file with no `mcpServers` → empty; invalid JSON → 500 from the outer try, so corruption surfaces rather than silently emptying the list). New integration tests pin the 500 path on both GET and POST. Nits: - Add --inspector-surface-subtle token, use it in ServerRemoveConfirmModal instead of the raw Mantine var. - Rename useServers buildHeaders boolean param to includeJsonBody. - Add a comment on the env-value vs header-value trim asymmetry (env values preserve trailing whitespace; header values follow RFC 7230). Tests: 1630 unit + 404 integration + 306 storybook all green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(ServerConfigModal): null currentTarget in functional setState updater React nulls SyntheticEvent.currentTarget after the synchronous handler returns. Reading e.currentTarget.value inside `setForm((f) => ...)` worked under happy-dom (which keeps currentTarget around) but threw "Cannot read properties of null (reading 'value')" on the first keystroke in a real browser. Capture the value before the functional updater closes over it. All seven onChange handlers in ServerConfigModal had the bug. Extended the AddEmpty storybook play to type into Server ID and Command — the real-Chromium runner now exercises this path so a future regression fails the test suite instead of the user. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(remote-server): surface stderr + transport_error when subprocess crashes during startup A stdio subprocess that spawns successfully but exits before the browser opens the SSE events stream used to vanish into a bare "Session not found" 404. The fix has three parts: 1. RemoteSession.markTransportDead always pushes the transport_error event (via pushEvent's queue) rather than dropping it when no consumer is attached. The queued event drains when the consumer eventually arrives. 2. /api/mcp/connect's transport.onclose handler no longer deletes the session immediately when no consumer is attached. Instead it holds the session (with queued stderr + transport_error) for a 30s grace window so the events endpoint can drain it. A timeout sweeps if no consumer ever connects. 3. /api/mcp/events detects isTransportDead after attaching the consumer (which just drained the queue), yields once to flush, then closes the stream and deletes the session. End result: the user sees the actual error (e.g. "Cannot find module" from stderr + the transport-closed reason) instead of a confusing 404. Tests: - New unit tests (clients/web/src/test/integration/mcp/remote/remote-session.test.ts) cover the queueing behavior, live delivery, and clearEventConsumer signaling under all transport-state permutations. - New integration test (connect-crash.test.ts) spawns a node subprocess that writes to stderr and exits, then verifies the events stream delivers both the stderr line and the transport_error event before closing — the exact scenario from PR #1349. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(state): gate listRequestorTasks on the server's `tasks` capability ManagedRequestorTasksState was calling tasks/list on every connect. Servers that don't advertise the `tasks` capability (the common case) reply with -32601 "Method not found", which then surfaces in the inspector's console for every connect. Skip the call when the server doesn't declare `tasks` — empty tasks list is the right semantics. Existing tests opt into capabilities so the live-path coverage is preserved; two new tests pin the gated behavior (direct refresh + connect-event-triggered refresh). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(App): clear activeServerId on disconnect so other server cards re-enable ServerCard dims any card whose id differs from `activeServer` (and marks it inert), so cards stay non-interactive after a disconnect until the user reconnects elsewhere. activeServerId was set on connect but never reset on disconnect, leaving the whole screen locked to the just- disconnected card. Subscribe to InspectorClient's `disconnect` event and clear activeServerId there. Covers all three disconnect paths uniformly: - user toggles the connected card off - user clicks the header Disconnect button - mid-session transport failure (subprocess exit / network drop) Event-based instead of effect-based on connectionStatus to avoid the first-render-clobbers-new-id trap (status starts as "disconnected" for a freshly-created client before connect() runs). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
e9ac57c548 |
docs(spec): add v2 server-list file design (#1343) (#1344)
Design for replacing the hardcoded SEED_SERVERS in App.tsx with a file-backed list at ~/.mcp-inspector/mcp.json, reusing the v1.5-ported storage primitives and the canonical mcp.json parser. Ties the new doc into the Storage section nav as the first sub-menu item. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
8d6dea0af2 |
Port resource preview UX improvements to PromptsScreen (#1328) (#1331)
* Port resource preview UX improvements to PromptsScreen (#1328) Brings the prompt-fetching flow up to parity with the resource preview work from #1326: - Cap the argument-form pane at maw=40% so a bare input + button doesn't stretch across wide displays. - Auto-fetch no-argument prompts the moment they're picked from the sidebar (handleSelectPrompt routes them straight to onGetPrompt({})), and hide the form once the user clicks Get Prompt — the result panel takes the same fixed-height column the resource preview uses. - Apply the PreviewCard variant=preview pattern: card sizes to content but caps at viewport, PromptMessagesDisplay pins its header (flex 0 0 auto) and lets the inner ScrollArea (flex 0 1 auto, mih 0) absorb overflow. - Tag getPromptState with the prompt name in App.tsx so the screen can ignore a stale result whose name no longer matches the selection. - MessageBubble now routes each content block through ContentViewer (markdown for text via mimeType="text/markdown", image/audio/resource via existing ContentViewer branches). Non-renderable block types (tool_use, tool_result) are filtered out; the role-label header keeps the bubble visible regardless. - PromptArgumentsForm gains onCompleteArgument + completionsSupported; when both are set, each input becomes Mantine Autocomplete with the same per-arg AbortController + per-arg debounce timer pattern as ResourceTemplatePanel. PromptsScreen + InspectorView + App.tsx wire the callback to inspectorClient.getCompletions with a ref/prompt envelope. Closes #1328. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Add close (X) buttons to ResourcePreviewPanel and PromptMessagesDisplay A top-left CloseButton dismisses the preview panel. The host screen decides what to fall back to: - ResourcesScreen remembers the originating template URI when the user reads from a template form (originatingTemplateUri); closing the preview restores the template form. Plain-resource reads (sidebar selection) just empty the selection and return to the empty state. Picking a different template / resource from the sidebar clears the back-trail. - PromptsScreen flips submittedFor back to undefined when the closed prompt has arguments — the argument form re-renders with the user's values preserved so they can edit and re-submit. No-arg prompts have nothing to fall back to, so the selection is dropped and the empty state appears. The X button is hidden when the host omits onClose, so callers that don't want a dismiss control keep their existing rendering. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Add close button to pending and error preview states The OK-state preview already had its X button (inside the panel component itself). Pending and error states were inline cards in renderReadState / renderPreview with no way to dismiss them, so a user who submitted bad input to a resource template or prompt was stuck staring at the error until they picked a different sidebar item. Adds a top-left CloseButton row to both states on both screens, wired to the same handleClosePreview handler — so the template form or argument form re-appears on close just like it does from the OK state. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Fire prompt completions on focus and send every sibling in context Two fixes to the PromptArgumentsForm autocomplete: - Focusing an argument input now fires completion/complete immediately (handleFocus) — the dropdown is populated as soon as the user clicks in, not only after they start typing. Any in-flight debounce timer for the same arg is cancelled so a stale keystroke request can't overwrite the fresh focus response. - The completion context now includes every declared prompt argument (with "" for ones the user hasn't typed into yet), minus the one being completed. Previously the context only carried args the user had touched, so servers that disambiguate based on co-arguments couldn't see the full picture on the first keystroke. Tests cover both: a focus-only path that fires before any keystroke, and a typing path that asserts the empty sibling is sent through. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Fire resource-template completions on focus Mirrors the prompt-side change: focusing a template variable input now fires completion/complete immediately so the dropdown populates the moment the user clicks in, rather than waiting for the first keystroke + 300ms debounce. Any pending debounce timer for the same variable is cancelled first so a stale keystroke response can't overwrite the focus response. The sibling-context coverage was already correct here — `variables` is seeded with empty strings for every declared template variable at mount and on template switch, so the context payload always carries the full variable set minus the one being completed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Style Autocomplete dropdowns to match the rest of the app Two fixes so the completion dropdowns on prompt-argument and resource-template inputs look like every other dropdown in the app: - App.css: the dark-mode dropdown / option-hover rules used to target .mantine-Select-* only, so the Autocomplete dropdowns shipped with Mantine's default surface color and a different hover background. Re-scoped to .mantine-Combobox-* so every Combobox-built input (Select, Autocomplete, MultiSelect, …) inherits the same styling. - theme/Autocomplete.ts: new theme module setting `radius: "md"` so Autocomplete matches the default Select / TextInput corner radius. Wired through theme/index.ts and theme/theme.ts. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Fix dropdown styling — Select and Autocomplete need separate selectors The previous commit re-scoped the dark-mode dropdown rules from `.mantine-Select-*` to `.mantine-Combobox-*` on the assumption that both Select and Autocomplete share a Combobox class on the dropdown root. They don't — Mantine's Combobox factory takes `__staticSelector` from the parent component, so Select emits `mantine-Select-dropdown` and Autocomplete emits `mantine-Autocomplete-dropdown` with no shared `Combobox` class on that element. The `.mantine-Combobox-*` selector matched nothing, so the TaskControls filter (and every other Select) lost its gray surface and reverted to Mantine's default brown-ish dark background. List both selectors so Select dropdowns keep their styling and Autocomplete dropdowns pick it up. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Address review feedback: canSubmit, fire-time context, re-click guard - PromptArgumentsForm: add a canSubmit guard that disables Get Prompt until every required argument has a value, matching the resource template form's symmetry. Optional args may stay blank. - Both forms: capture sibling context inside the debounce callback, not at schedule time. Hold the latest argumentValues / variables in a ref + sync via useEffect, and call buildContext() at fire time. Without this, typing in arg A then arg B within the 300ms window would ship A's request with B's pre-keystroke value. - PromptsScreen.handleSelectPrompt: early-return when the user re-clicks the already-selected prompt — sidebar is for navigation, ✕ is for dismiss. Previously a re-click wiped form values and re-fired the auto-fetch for no-arg prompts. - MessageBubble: expand the comment on effectiveMimeForBlock to flag the unconditional markdown promotion as a known asymmetry with the resource side (which only promotes when the server signals it), pending a per-block mimeType in the spec. - New tests: canSubmit disabled / enabled transitions, fire-time context capture across siblings, abort-path verification (a stale in-flight response must not overwrite the fresh one), and the no-op re-click guard on PromptsScreen. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Address follow-up review nits: ghost suggestions, dead previewActive branch - Clear `completions[name]` at the top of handleChange in both PromptArgumentsForm and ResourceTemplatePanel so the dropdown doesn't show stale options from the previous prefix during the 300ms debounce + network window. - Simplify previewActive in PromptsScreen to drop the unreachable `!getPromptState?.promptName` fallback. App.tsx tags every prompt state transition with `promptName`, so the fallback was dead code that obscured the actual runtime invariant. - Add a focused test for each form that asserts stale dropdown options vanish the instant a new keystroke arrives. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
6678313373 |
Expose live resource subscriptions via a hook (#1325) (#1326)
* Expose live resource subscriptions via a hook (#1325) Add ResourceSubscriptionsState that mirrors InspectorClient's subscribed URIs as InspectorResourceSubscription[], resolving each URI against the managed resources list (so the subscription tile shows server-supplied name/title) and stamping lastUpdated on notifications/resources/updated. Pair it with useResourceSubscriptions and wire App.tsx so the Resources screen reflects subscribe/unsubscribe actions in real time. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Address review notes on resourceSubscriptionsState - Soften the onResourceUpdated comment: the client's dispatch is already guarded by subscribedResources.has(uri), so the re-check is true defense-in-depth rather than guarding a known hazard. - Use this.getSubscriptions() in the statusChange handler so every emit goes through the defensive-copy path. - Document the deliberate fallback to a synthetic Resource when a previously-listed resource is removed from the managed list while the user is still subscribed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Reflect subscribed state in preview + compact subscribed tile labels - Wire `isSubscribed` on the ReadResourceState passed to ResourcesScreen by deriving it from the live subscriptions list in App.tsx. The ResourcePreviewPanel's SubscribeButton already flips its label to "Unsubscribe" when subscribed; without this derivation isSubscribed was always false and the button looked stuck on "Subscribe". - In ResourceSubscribedItem, display only the last URI path segment (truncated with ellipsis if it still overflows) and surface the full URI via a hover tooltip. Keeps the Subscriptions pleat readable when resources have long path-style URIs. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Auto-read resource on sidebar click Clicking a resource in the URIs accordion now triggers onReadResource in addition to setting the selection, so the preview panel jumps straight to the pending → ok render path. Removes the unreachable "Click to read this resource" placeholder, since the loader / preview / error states always cover the rendered output for a selected resource. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Render markdown resources via react-markdown + fix preview overflow - Add react-markdown / remark-gfm dependencies. - ContentViewer accepts an optional mimeType prop; when it matches text/markdown (or text/x-markdown), text blocks render via react-markdown wrapped in a `.markdown-content` container. Non-markdown text now uses the existing Code "wrapping" variant so long lines stay inside the panel. - ResourcePreviewPanel infers `text/markdown` from a `.md` / `.markdown` URI suffix when the server didn't supply a mimeType, and threads the effective mime through to ContentViewer per content item. - ResourcesScreen flex children get miw=0 so a long unwrappable line in the resource body can no longer push the preview past the viewport's right edge. - App.css gains a `.markdown-content` ruleset constraining nested pre, table, code, and img elements to the container's width. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Contain resource preview: sticky header/footer, scrollable body Restructure ResourcePreviewPanel into a fixed-height column with the resource title + URI pinned to the top and the timestamp / annotations / subscribe-refresh actions pinned to the bottom. The content viewer area in the middle now owns its own ScrollArea, so a long markdown body scrolls within the panel instead of pushing the subscribe button below the viewport. In ResourcesScreen, the selectedResource branch (and the template branch's right pane) now hosts the panel inside a PreviewPane Flex column with the screen's max-height, and renderReadState wraps in a FillDetailCard sized to fill that column. The legacy outer ScrollArea.Autosize is removed for these panes since scrolling is internal now. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Auto-hide template after read; size preview to content with cap - After the user clicks Read Resource on the template form, handleReadResource now clears selectedTemplateUri so the screen swaps from the template form to the resource preview. Previously both panels stayed mounted side-by-side. - The preview no longer hard-fills the viewport. The Card uses a new "preview" theme variant (overflow: hidden) and is content-sized, capped at SCROLL_MAX_HEIGHT. ResourcePreviewPanel's flex column marks the header / meta / footer rows as `flex: 0 0 auto` and the ContentScroll as `flex: 0 1 auto` with `mih: 0`, so: - short content → card hugs it, footer sits right under the body - long content → card caps at viewport, ContentScroll shrinks and scrolls internally, footer stays pinned at the cap - ScrollArea / Group imports in ResourcesScreen pruned to match the simpler single-pane layout. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Cap template form panel at 40% width A bare text input + Read Resource button stretched across the whole main content area looks awkward on wide displays. Apply maw=40% to the PreviewPane in the template branch so the form keeps a reasonable form-field width regardless of viewport. The preview branch is unaffected — resource bodies still get the full main area for content. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Live completion/complete suggestions on resource template inputs Wires the server's `completions` capability through to the resource template form so each variable's input becomes an Autocomplete that fires `completion/complete` (debounced, 300ms) on every keystroke and surfaces the returned values as a dropdown — mirroring v1's behavior. - core/mcp/inspectorClientProtocol.ts: surface getCompletions on the protocol so non-runtime callers (state managers, hooks, tests) can depend on it. FakeInspectorClient gains a vi.fn-backed stub. - ResourceTemplatePanel: accepts onCompleteArgument + completionsSupported. When both are present it renders Mantine Autocomplete instead of TextInput, debounces keystrokes via per-arg timers, aborts in-flight requests on the next keystroke, and disables client-side filtering (the server already filtered for the typed prefix). - ResourcesScreen / InspectorView: thread the props through; the screen-level callback re-injects the active template's URI as the `ref: "ref/resource"` so the panel-level callback stays ref-free. - App.tsx: wires onCompleteArgument to inspectorClient.getCompletions and derives completionsSupported from `capabilities?.completions`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
4d74b94bed |
Wire App.tsx to v2 core/ hook layer; remove demo stub from InspectorView (#1322)
* feat(web): wire App.tsx to v2 core/ hook layer; remove demo stub from InspectorView
App.tsx mounts InspectorView with live data from the v2 hook layer instead
of the title-page placeholder. InspectorView becomes pure prop-driven; the
demo handshake stub and connection state machinery are removed.
Connection lifecycle:
- App.tsx lazily instantiates `InspectorClient` on the connect edge,
rebuilding it (and its state managers) when the user picks a different
server. State managers (Managed{Tools,Prompts,Resources,ResourceTemplates,
RequestorTasks}State + MessageLog/FetchRequestLog/StderrLog) are
destroyed on switch.
- `useInspectorClient` + the per-primitive `useManaged*` hooks drive the
view's data. `latencyMs` is captured at the connecting → connected edge
via a ref so the intermediate rerenders don't lose the start timestamp.
- `errorMessage` is captured from `client.connect()` rejection; status-
machine-driven errors that don't surface through the promise are a known
follow-up (no `error` event in the v2 InspectorClientEventMap yet).
Action handlers:
- `onCallTool` / `onGetPrompt` / `onReadResource` await the corresponding
InspectorClient method and reflect pending → ok/error in `ToolCallState`
/ `GetPromptState` / `ReadResourceState` panel props. Manually verified
end-to-end against `npx @modelcontextprotocol/server-filesystem` —
the Tools result panel renders the real "Allowed directories: /private/tmp"
output rather than a stub.
- `onSetLogLevel` calls `client.setLoggingLevel(level)` and optimistically
bumps `currentLogLevel` locally (the request has no echo notification).
- `onSubscribeResource` / `onUnsubscribeResource` / `onCancelTask` route to
their respective client methods. Server CRUD (`onServerAdd`/`onServerEdit`
/…) and history pinning are intentionally `todoNoop` for now; the seed
server list is hardcoded per the agreed scope (a `useServers` v2-only
hook is a separate effort).
- `notifications/message` notifications flowing through `MessageLogState`
are filtered into `LogEntryData[]` and passed to the Logs screen, so
`logging/setLevel` and the resulting log stream round-trip.
InspectorView (`InspectorView.tsx`):
- Removed: `STUB_*` constants, `handshakeTimer` ref, `handleToggleConnection`
/ `disconnect` functions, stub `bridgeFactory`/`sandboxPath`, internal
connection state, `logLevel` state.
- Added 38 props covering connection state, panel states, log level, and
action callbacks. The view is pure presentational.
- `availableTabs` is derived from `connectionStatus`; `activeTab` is
clamped to whatever's currently available (avoiding the `set-state-in-effect`
lint and keeping the previously-selected tab on reconnect).
Stories + tests:
- `InspectorView.stories.tsx` adds spy callbacks for every action prop and
new `Connected` / `ConnectionError` stories for the connected/error
narratives.
- `InspectorView.test.tsx` is rewritten around the prop-driven contract
(10 tests covering the empty list, server cards, toggle dispatch,
connected header, error banner, tab snap-back on disconnect, app
filtering, log-level dispatch, autoScroll local toggle).
Dev backend port wiring (`vite.config.ts`):
- Vite's dev server is now pinned to `CLIENT_PORT` (default 6274) with
`strictPort: true`, matching the dev backend's `allowedOrigins`. Without
this, Vite would fall back to 5173 while the Hono origin check still
required 6274 — breaking every browser `/api/*` fetch out of the box.
- Top-level `resolve.alias` now spreads the same bare-module aliases the
vitest projects use. Rolldown can't reach `pino/browser.js` /
`zustand/middleware` from `core/`'s parent (no node_modules there); the
wired App pulls those modules into the browser dep graph for the first
time, so the aliases need to be visible at build time too.
createWebEnvironment helper (`src/lib/environmentFactory.ts`):
- Mirrors v1.5's `lib/adapters/environmentFactory.ts`: builds
`InspectorClientEnvironment` from `createRemoteTransport` +
`createRemoteFetch` + `createRemoteLogger` + `BrowserOAuthStorage` +
`BrowserNavigation`. Returns the assembled environment plus the logger
for direct app-level use.
Closes #1244.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(web): wire dev-backend auth token into createWebEnvironment + show full stdio command in ServerCard
Two issues surfaced after the #1244 wiring landed locally:
1. **401 on `/api/mcp/connect`** when running `npm run dev` with the
default auth (no `DANGEROUSLY_OMIT_AUTH`). The dev backend generates a
random token and prints `http://localhost:6274?MCP_INSPECTOR_API_TOKEN=…`
in the banner, but App.tsx was calling `createWebEnvironment(undefined, …)`
so the browser never sent `x-mcp-remote-auth`. The Hono middleware
correctly rejected with "Authentication required. Use the
x-mcp-remote-auth header with Bearer token."
Adds a small `getAuthToken()` helper in App.tsx that reads
`MCP_INSPECTOR_API_TOKEN` off `window.location.search`, falls back to
`sessionStorage`, and persists fresh values into sessionStorage so
client-side navigation / OAuth round-trips don't drop the token. The
resolved value is passed to `createWebEnvironment` whenever a new
InspectorClient is built.
2. **Misleading ServerCard display** — the seed server's `config` is
`{ command: "npx", args: ["-y", "@modelcontextprotocol/server-filesystem",
"/tmp"] }` but the card only rendered `"npx"`, so it looked like the
command was incomplete. `getCommandOrUrl` now joins `command + args`
for stdio configs, so the card shows the same string that gets spawned.
SSE / streamable-http paths unchanged.
Manual end-to-end re-verified against `npm run dev` (no DANGEROUSLY_OMIT_AUTH):
- Banner prints the auth-token URL.
- Browser navigates to that URL → `getAuthToken()` picks the token off the
query string, persists to sessionStorage, threads it through to every
`/api/*` request.
- Server card shows `npx -y @modelcontextprotocol/server-filesystem /tmp`.
- Toggle connect → connection succeeds; ViewHeader shows
`Connected (~1.3s)`; Tools tab populates with the 14 filesystem tools.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(web): use dark-9 instead of blue-9 for dark-mode page background
`main.tsx` overrode `--mantine-color-body` to `--mantine-color-blue-9` in
dark mode, while `.storybook/preview.tsx` used `--mantine-color-dark-9`.
The dev app then rendered with a blue background while every story (and
the design intent visible in storybook) used dark grey.
Switch main.tsx to `--mantine-color-dark-9` so the dev app matches the
storybook reference.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(web): seed the Servers screen with the "everything" reference server
Adds `@modelcontextprotocol/server-everything` to the hardcoded seed list
alongside the filesystem server. The everything server exposes tools,
prompts, resources, sampling, and completion — covering most of the
surface a developer might want to exercise against the wired-up
InspectorView without configuring anything.
Comment updated to explain the two-seed shape.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(web): address #1244 PR review feedback (JsonValue cast, client lifecycle, dead code)
- **#1 — Misleading `Record<string, never>` cast**: Imported `JsonValue`
from `@inspector/core/mcp/index.js` and changed `args as Record<string,
never>` to `args as Record<string, JsonValue>` in `onCallTool`. The new
cast honestly narrows from the screen-level `Record<string, unknown>`
to what `InspectorClient.callTool` actually requires. Added a comment
explaining the boundary.
- **#2 + #3 — Previous InspectorClient not disconnected on server switch
or unmount**: Added a `useEffect` keyed on `inspectorClient` whose
cleanup calls `inspectorClient.disconnect()`. One effect covers both
the swap-on-switch leak (prior session's transport stayed open until
GC) and the unmount-during-connected leak (HMR / tests).
`setupClientForServer`'s existing state-manager `destroy()` calls
handle the listener side; the new effect handles the transport side.
- **#4 — Dead `void` block at bottom of file**: Deleted the trailing
`void FetchRequestLogState; void StderrLogState;` block. The state
managers are already constructed via `new FetchRequestLogState(client)`
/ `new StderrLogState(client)`, so the imports are referenced by the
linter's reckoning. The `void` pair was dead code with a comment that
no longer matched reality.
- **#5 / #7 / #8 — Follow-up TODOs**: Added TODO refs to the new
follow-up issues:
- TODO(#1323): `error` event in InspectorClientEventMap so mid-session
transport failures surface (connect-only catch is incomplete).
- TODO(#1324): negotiated `protocolVersion` through `useInspectorClient`
(currently hard-coded `"2025-06-18"`).
- TODO(#1325): `useResourceSubscriptions` hook so subscribe/unsubscribe
buttons reflect their server-side effect.
Items #6 (useCallback dep churn), #9 (helper unit tests), and #10
(shared no-op bridge factory fixture) are acknowledged in the PR reply
as deferred — no functional impact and out of scope for a "wire the
hook layer" PR.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
37a4d99575 |
Port v1.5 web dev backend (vite-hono-plugin + remote-server bootstrap) (#1321)
* feat(web): port v1.5 dev backend (vite-hono-plugin + sandbox + remote-server bootstrap) Ports the v1.5 in-process dev backend into v2 so `npm run dev` serves the Hono remote-server (`/api/*`) alongside the Vite-served browser app. Without this, the browser-side `createRemoteTransport` had nothing to talk to during local development. Adds under `clients/web/server/`: - vite-hono-plugin.ts (Hono middleware on the Vite dev server; gated to `apply: 'serve'` and skipped when `process.env.VITEST` is set so it stays inert in vitest/storybook runs) - sandbox-controller.ts (standalone HTTP server for the MCP Apps sandbox) - web-server-config.ts (env parsing + initial-config payload + banner; `autoOpen` defaults off under VITEST so test runs don't shell out to `open`) - server.ts (standalone Hono prod server for the future v2 launcher, #1246) - start-vite-dev-server.ts (in-process Vite starter for the future launcher) - vite-base-config.ts (shared optimizeDeps exclusions) Wires `honoMiddlewarePlugin(buildWebServerConfigFromEnv())` into `vite.config.ts` with `apply: 'serve'`. Adds the `open` dependency and a DOM lib + `@inspector/core/*` path alias to `tsconfig.node.json` so the dev-backend files type-check alongside vite.config.ts. Coverage: web-server-config.ts (100% lines), sandbox-controller.ts (91% lines), and vite-base-config.ts (100%) all clear the per-file gate. The Vite plugin, prod server, and dev-server starter are excluded from coverage as runtime glue exercised by `npm run dev` end-to-end (callers covered separately; their logic lives in core/mcp/remote/node, which integration tests already exercise). Closes #1320. Unblocks #1244 (Wire App.tsx) — its test plan can now run end-to-end. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(web/server): address PR review feedback on the dev-backend port - sandbox-controller: resolve start() with empty values on listen error (e.g. EADDRINUSE on a fixed MCP_SANDBOX_PORT) instead of leaving the promise pending forever. Without this, the Vite plugin's `await sandboxController.start()` could hang the entire dev backend on a port collision. Banner now silently omits the sandbox line; getUrl() keeps returning null. Adds a regression test exercising the EADDRINUSE path against a real socket. - vite-hono-plugin: listen for `error` and `close` on the incoming request stream (in addition to `end`) so an aborted POST/PUT (browser navigates mid-upload) doesn't leak the middleware indefinitely. - vite-hono-plugin: add a `.catch()` to the initial `pump()` kickoff so a sync throw before the first `await reader.read()` can't surface as an unhandled rejection. - sandbox_proxy.html: drop the stale CSP comment — v1.5's sandbox HTTP server never set CSP either; this PR doesn't regress anything. Replace with the actual isolation model (iframe sandbox attribute + origin allowlist) so future readers don't assume a CSP header is enforced. - Hoist `WebServerHandle` to a shared `server/types.ts` so the future launcher port (#1246) doesn't need to pick one of two duplicate declarations. - Extract `resolveAutoOpen()` helper out of the triple-nested ternary in `buildWebServerConfigFromEnv` for legibility, with the three-way logic documented in one place. - Add an inline comment explaining why `webServerConfigToInitialPayload` falls back to streamable-http for unknown discriminators (forward-compat with future SDK transports) rather than throwing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
3569321ceb |
test: share remote-server setup across response-shape describes (#1313) (#1319)
Hoist startRemoteServer(0) out of per-it() bodies into describe-scope beforeAll/afterAll for the authentication, dangerouslyOmitAuth, endpoint-error-paths, and Origin-validation describes — every it() in these blocks only fires a single request and asserts response shape, so one shared server per describe is safe. Storage and additional-coverage describes are unchanged: their tests mutate per-test filesystem state at common paths or use heterogeneous server options, so sharing would require reorganizing the suite (out of scope per the issue). Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |