Extracts the Ace integration #2094 carried inline into a shared
`elements/JsonEditor`, and renders it on every surface where the web
client types or displays JSON.
The element is deliberately text in, text out. The two editing contracts
above it disagree about what an unparseable draft means — JsonObjectInput
keeps the last valid object and says nothing to its parent, SchemaJsonField
reports `undefined` and blocks submission (#2020) — so parsing, and the
draft/value split that goes with it, stays with each caller.
Surfaces: SchemaJsonField (inherited by all four SchemaForm consumers), a
new "Edit as JSON" switch for the whole arguments object, the server.json
File Contents box, the raw JSON-RPC request box, ContentViewer's JSON
branch (read-only, inherited by its eleven consumers), and a new "Edit and
replay" modal on a Protocol entry.
Ace read-only replaces the CodeHighlight JSON path and the Prism `json`
grammar loader is dropped: folding, not colorization, is what it adds, and
two highlighters for one language drift. `wrap={false}` callers and untyped
text that only looks like JSON keep the plain renderer.
Closes#2151
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UL9retfvAXRgvi6EWk4SY4
Signed-off-by: cliffhall <cliff@futurescale.com>
A field that cannot turn its text into a value reports `undefined` — which is
exactly what an empty field reports. That made the two indistinguishable to
every caller of `SchemaForm`, so invalid text in an optional field was
submittable and the argument simply arrived absent (#2020).
`SchemaForm` now tracks per-field draft validity and reports it through
`onValidityChange`, covering both fields that hold text the form cannot send:
the JSON editor (unparseable text) and the number input (a magnitude JS cannot
hold exactly, which is dropped rather than silently rounded). The number field
also states it on itself, as the JSON editor already did. A nested object form
reports through the same channel, and both the field-level and form-level
effects report "valid" on cleanup so a draft cannot outlive the field that held
it — a remount under a new `resetKey`, or an unmounted panel.
`ToolDetailPanel` gated Execute on `isExecuting` alone, so it also had no
required-argument check at all — unlike the App and elicitation panels. It now
uses `hasMissingRequiredFields` like they do. `InlineElicitationRequest` had
neither gate and gets both.
Signed-off-by: cliffhall <cliff@futurescale.com>
Review round 2. Three findings, all real.
1. Precision. Mantine's `NumberInput` returns the raw *string* rather than
a number once the value reaches `Number.MAX_SAFE_INTEGER` (see its
`isValidNumber` guard), precisely because JS cannot hold it. Parsing
that turned `90071992547409910` into `90071992547409904` and would
have sent the server a number the user never typed. An inspector must
not misreport what it transmits, so such input now reports no value —
which is also what this field did before #1888, so nothing regresses.
Only the integer part overflows, so long decimals stay parsed.
2. `AppDetailPanel` has the same reuse pattern as `ToolDetailPanel`:
`AppsScreen.handleSelect` swaps `selectedAppName` + `formValues` in
place without remounting, so switching between apps with a same-named
number field could retain an equal-valued draft. It now passes
`resetKey` too.
3. An exponent was never reachable: `NumberInput` masks input through
`NumericFormat`, which rejects `e`. Dropped that claim from the doc
comment rather than implying `"1e"` is a case being handled.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0119LaPQ3v4NyBK3ZTdM9j84
Migrate all three clients + core + test-servers from
@modelcontextprotocol/sdk@1.29 to SDK v2 (2.0.0-beta.4), keeping behavior
identical against legacy (2025-11-25) servers. Foundation for the SDK V2 +
New Spec stack.
Packages
- Split @modelcontextprotocol/sdk -> @modelcontextprotocol/client (Client,
transports, OAuth, error taxonomy, protocol types) + core (Zod schemas);
stdio -> client/stdio, Ajv -> client/validators/ajv. Test-servers ->
@modelcontextprotocol/server (+ server-legacy/sse). Deterministic import
rewrite (schemas -> core, everything else -> client); the codemod misroutes
client code to /server so it was not used for path routing.
- new Client(..., { versionNegotiation: { mode: 'legacy' } }) — nothing
2026-era goes on the wire.
Runtime (core/mcp/inspectorClient.ts)
- Error taxonomy: McpError -> ProtocolError, ErrorCode -> ProtocolErrorCode;
RequestTimeout -> SdkError/SdkErrorCode.
- Handlers take method strings; task methods (tasks/*, notifications/tasks/*)
use the 3-arg custom form (excluded from v2's spec-method set).
- Legacy tasks kept working (per project requirement): requestor tasks re-
implemented over raw client.request + explicit deprecated schemas;
experimental.tasks.callToolStream replaced by a local pollTaskToolCall
async generator (with _progressHandlers revival so task-execution progress
still reaches the caller). Receiver (server-initiated) tasks: the v2 Client
validates a sampling/elicit handler result and rejects the legacy { task }
response with -32602 — installReceiverTaskResponseBypass swaps the wrapped
entry in the private _requestHandlers map for the task-augmented branch so
{ task } reaches the wire (fixes server-initiated sampling-via-task).
- Pagination: list verbs use raw client.request({method}, List*ResultSchema)
for single-page behavior (v2's client.listX() auto-aggregate all pages).
Web / auth / apps
- Rename local JsonSchemaType -> InspectorFormSchema + add toFormSchema()
structural narrowing at the SDK-schema -> form boundary.
- ext-apps still peers on SDK v1: one compat boundary with documented casts
(remove when ext-apps#702 lands a v2 peer).
- OAuth provider saveClientInformation reconciled with v2's
OAuthClientInformationContext; InvalidClientMetadataError re-homed as a
local class (server-legacy/auth is not browser-safe).
- vitest.shared.mts dedupes @modelcontextprotocol/client+core to one copy so
vi.mock intercepts the SUT's import.
Accepted v2 behavior changes (per #1624 / spec §9.2): unknown-tool call
rejects with -32602; cancellation is stream-abort (no guaranteed
notifications/cancelled frame); output-schema strictness restored via the
Inspector's own Ajv check on the default path.
npm run ci green: validate (web 3087 / cli 212 / tui 274 / launcher 5),
coverage (>=90 per-file, all clients), smoke (launcher/cli/web), storybook
(410).
Closes#1624
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(apps): AppsScreen and InspectorView wiring (#1263)
Adds the Apps tab as a top-level screen composed of `AppListItem`,
`AppDetailPanel`, and `AppRenderer`. The screen is presentational —
selection, form values, running, and maximize state are local; data and
side effects come in via props and callbacks.
InspectorView filters `tools` through `isAppTool` (wrapped in a try/catch
so a malformed `_meta.ui.resourceUri` skips one tool instead of dropping
every following App), exposes the Apps tab in `ALL_TABS` after Tools, and
wires Phase-3 stub callbacks plus a stub bridge factory.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(apps): extract AppControls and rename Refresh button
Pulls the sidebar list, search input, "List updated" indicator, and
Refresh button out of `AppsScreen` into a dedicated `AppControls`
group, mirroring `ToolControls` and `TaskControls`. The Refresh button
moves from a full-width row beneath the list to the top-right of the
title row (single label "Refresh", matching `TaskControls`).
`AppsScreen` keeps the right-pane orchestration (selection, form,
running, maximize, header actions) and now delegates the entire
sidebar to `AppControls`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(stories): seed InspectorView demo tools with MCP Apps
Adds three apps (Cohort Data, Weather Widget, Ops Dashboard) carrying
real `_meta.ui.resourceUri` to the demo tools list so the Apps tab has
content in the Default story alongside the existing tools.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(apps): drop AppDetailPanel title row to avoid duplicate name
The parent AppsScreen header already shows the app name, so the panel's
own title/icon row rendered the name twice. Remove the title row (and
its now-unused imports, styled subcomponents, and the four tests that
asserted on title/icon rendering).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(resources): drop annotation badges from ResourceListItem
The list row now renders just the resource title/name. Audience and
priority badges remain visible in the preview/template panels, so the
data is still surfaced — just not duplicated in the picker. Drops the
now-unused AnnotationBadge import, the badge-only stories, and the
badge-rendering test.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(apps): address review — share hasInputFields, show header icon
- Extracts the duplicated `Object.keys(inputSchema.properties ?? {}).length > 0`
check from `AppsScreen` (and the equivalent in `AppDetailPanel`'s
`hasFields`) into a shared `hasInputFields` helper in `toolUtils`,
with tests.
- Renders the selected tool's first icon next to the right-pane header
title in `AppsScreen`, restoring the icon visibility that was lost
when `AppDetailPanel`'s title row was removed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(apps): finish hasInputFields extraction; document isOpening
- AppDetailPanel now actually calls the shared `hasInputFields` helper
instead of inlining the same `Object.keys(...).length > 0` check.
- AppsScreen comments why `isOpening={false}` is hardcoded: the screen
flips to AppRenderer synchronously, so the loading state is unreachable
here — the prop remains for standalone use and Phase 3 wiring.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(apps): add AppListItem and AppDetailPanel groups (#1261)
Two presentational groups for the upcoming AppsScreen.
AppListItem (clients/web/src/components/groups/AppListItem/):
- Props { tool, selected, onClick }
- Renders tool.icons[0] (sized sm) when present, label (title ?? name),
two-line clamped description, and a trailing chevron affordance.
- Reuses ToolListItem's selected-state highlight (UnstyledButton bg via
--mantine-primary-color-light).
AppDetailPanel (clients/web/src/components/groups/AppDetailPanel/):
- Props { tool, formValues, isOpening, onFormChange, onOpenApp }
- Layout: optional icon + title row, optional description, divider,
SchemaForm bound to tool.inputSchema, full-width "Open App" button.
- "Open App" is disabled while isOpening or while any required input
field is empty (validation derived from tool.inputSchema.required).
- No annotations, progress, or cancel — execution is delegated to the
embedded app, not run as an MCP tool call from the panel.
Convention notes:
- Issue text references @tabler IconChevronRight / IconPlayerPlay; the
v2 web client already uses react-icons (md/ri namespaces), so this PR
uses MdChevronRight and MdPlayArrow for consistency rather than
introducing a second icon library.
Stories: AppListItem (Default, Selected, WithIcon, NoDescription, LongName);
AppDetailPanel (NoFields, SimpleStringParam, MultipleParams, WithIcon,
Opening, ComplexSchema).
Tests: 22 cases covering rendering branches, callback wiring, and the
disabled-button derivation. Per-file coverage 100% across lines,
statements, functions, and branches for both new components.
Closes#1261
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(stories): inline SVG for WithIcon stories so they render reliably
The Wikipedia-hosted PNG used in the AppListItem and AppDetailPanel
WithIcon stories returned a broken image in Storybook (rate-limited /
hot-link blocked). Replace it with an inline SVG sun icon as a data
URI so the stories render offline, in Chromatic, and on any network.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test(apps): address PR #1273 review feedback
- Extract resolveDisplayLabel(name, title?) to src/utils/toolUtils.ts and
migrate all four call sites (AppListItem, AppDetailPanel, ToolListItem,
ToolDetailPanel) to consume it. Avoids the fourth copy when the next
list/detail screen arrives. Includes a small unit suite.
- AppDetailPanel: only render the <Divider/> when the schema has at least
one property — previously a no-input app showed an orphaned separator
between the description and the Open App button.
- AppDetailPanel: align PanelTitle truncate to `true` (matches the
ItemLabel form used in AppListItem / ToolListItem); behaviorally
identical, just less surprising for future readers.
- AppDetailPanel: add an inline comment explaining that SchemaForm is
disabled only while isOpening, not while validation fails — users
should be able to keep filling required fields. The validation gate
lives on the Open App button.
- Extract the inline SUN_ICON_SVG data URI used by the WithIcon stories
to src/test/fixtures/storyIcons.ts so the next story that needs it
doesn't grow a third copy.
- Tests: add the AppDetailPanel divider-conditional cases (rendered
with fields, omitted without). Per-file coverage stays at 100% for
AppListItem, AppDetailPanel, ToolListItem, ToolDetailPanel, and the
new toolUtils. Suite: 70 files / 618 tests passing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test(toolUtils): rename empty-string test for readability
Drops the doubled "is ... is preserved" phrase in the test name.
Behavior unchanged.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>