Commit graph inspector/clients/web/src/components/groups/ToolDetailPanel/ToolDetailPanel.tsx
Author SHA1 Message Date
cliffhall
24a9786337 fix(web): reset a number field's draft text when the form changes entity
Review follow-up. The draft/value re-sync compares parsed numbers, so it
cannot see a reset to an *equal* value. `ToolDetailPanel` is not keyed by
tool, so its fields are reused across selections: type `-` (draft `"-"`,
value `undefined`), switch to a tool with a same-named number field and
no default, and the value is `undefined` on both sides — no divergence is
detected and the stale `-` stays in the box for the new tool.

Add a `resetKey` prop carrying the identity of whatever the form edits,
and vary the number field's React key with it so no in-progress text can
outlive the entity it was typed into. `ToolDetailPanel` passes the tool
name; nested object forms inherit it. The schema object is no substitute
— callers rebuild it every render, so its identity is unstable.

The regression test drives the switch with `fireEvent` rather than
`user.click`: a real click also blurs the input, and Mantine sanitizes an
incomplete value on blur, which masked the defect entirely. A paired test
asserts a *stable* `resetKey` does not remount, since remounting on every
render would wipe the draft and reinstate #1888.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0119LaPQ3v4NyBK3ZTdM9j84
2026-08-11 10:02:59 -04:00
Sam Morrow
5d91d4acd0
fix(core): mirror SEP-2243 x-mcp-header args to Mcp-Param-* on tools/call (#1847)
* fix(core): mirror SEP-2243 x-mcp-header args to Mcp-Param-* on tools/call (#1846)

A strict modern (2026-07-28) HTTP server (e.g. the GitHub MCP Server)
rejects a tools/call whose argument carries an `x-mcp-header` annotation
when the matching `Mcp-Param-*` request header is missing (-32020
"header mismatch"). The Inspector never sent that header.

The SDK only mirrors inside `client.callTool()` (and skips it entirely in
a browser environment), but the Inspector routes tools/call through
`client.request()` for manual MRTR driving (#1704), so the SDK never
mirrors for us — on any transport. On top of that, the remote (web)
transport dropped per-request headers before they reached the backend's
upstream fetch.

- core/json/xMcpHeader.ts: port the SDK's non-public `buildMcpParamHeaders`
  + value encoding (Base64 sentinel for non-ASCII/whitespace/empty values)
  and add `mcpParamHeadersForTool`.
- InspectorClient.attemptToolCall: on a modern connection, build the
  headers from the tool's declarations + call arguments and attach them to
  the tools/call request options. `Protocol.request` forwards `headers`
  to the transport and preserves them across MRTR retry legs, so the
  direct StreamableHTTP transport applies them. Fixes CLI/TUI.
- RemoteClientTransport.requestSend + /api/mcp/send + RemoteSendRequest:
  forward per-send headers and apply them to the backend's upstream
  transport.send. The browser can't set cross-origin headers, but the
  Node backend can — where the Inspector's real request is issued. Fixes web.

Verified end-to-end against the live GitHub MCP Server (modern HTTP).

Closes #1846

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 36dbeb09-12b0-4c19-af36-2573772a7dbd

* review: harden backend header allowlist, fix breadcrumbs, trim comments

Address Copilot review on #1847:
- server.ts: restrict client-supplied per-send headers to the `Mcp-Param-`
  prefix (string values only) via `mcpParamHeadersOnly` before applying them
  upstream, so the per-send channel can't inject arbitrary headers (e.g.
  Authorization). `settings.headers` remains the channel for configured
  upstream headers. Covered by new unit tests.
- Fix stale `#1810` breadcrumbs → `#1846` in the two new tests.
- Trim the verbose mirroring comments in inspectorClient/types/remote transport.
- Drop the redundant base64-on-the-wire integration test (encoding is unit-
  tested; wire delivery is proven by the verbatim test), remove two unneeded
  test casts, and cover the boolean `true` branch.

Not changed (declined with reason on the PR): buildMcpParamHeaders remains a
faithful port of the SDK's helper and does not re-validate values against the
declared JSON Schema type — arg types are enforced by schema validation on
both sides, and matching the SDK's conforming wire output is the module's goal.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 36dbeb09-12b0-4c19-af36-2573772a7dbd

* review: mirror Mcp-Param-* on the task-augmented tools/call too

Address the maintainer review on #1847:

- `callToolStream` (the "Run as task" path) built its own request options and
  never mirrored, so a task-augmented `tools/call` on an annotated tool was
  still rejected by a strict modern server with -32020. It now mirrors like the
  plain path.
- Extract the two blocks both `tools/call` entry points had duplicated —
  `convertStringToolArgs` (the string-arg coercion) and
  `applyMirroredParamHeaders` (the SEP-2243 mirroring) — so the paths can't
  drift again. That duplication is why the task path was missed.
- Regression test: `callToolStream` against the modern `get_weather` server
  asserts the POST carries `Mcp-Param-City` (verified failing without the fix).
- README: the modern-network showcase claimed mirroring is skipped in the
  browser so the web client gets -32020, and that CLI/TUI mirror. Both are now
  wrong — the Inspector mirrors itself, on every client. AGENTS: note that
  xMcpHeader.ts now also builds the wire headers.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UkhhCHryVp5H3dqEnhwZ7t

* docs: correct the stale "browser omits Mcp-Param-*" copy

Both statements predate this PR and are now the opposite of true, per the
wire capture on #1847 (proxy-observed upstream headers, web leg included).

- ToolDetailPanel: the mirrored-headers note claimed the browser omits
  `Mcp-Param-*`. It now says the web client's headers are applied by the Node
  backend that issues the upstream request.
- createRemoteFetch: the header comment claimed an `x-mcp-header`-annotated
  tool is "uncallable from the web client against a strict modern server" —
  the exact wrong conclusion to reach while debugging a -32020. It now records
  that the Inspector builds the headers itself, and that they do NOT travel
  this `/api/fetch` proxy path: they ride the transport's per-send `headers`
  through `/api/mcp/send`.

Copy only — no test asserted the old note text (ToolDetailPanel.test.tsx
matches the section title only).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UkhhCHryVp5H3dqEnhwZ7t

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: cliffhall <cliff@futurescale.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot-Session: 36dbeb09-12b0-4c19-af36-2573772a7dbd
2026-07-28 19:19:48 -04:00
Cliff Hall
e70a11360c
Tools: x-mcp-header annotation tooling + -32602 unknown-tool rendering (#1632) (#1735)
* Tools: x-mcp-header annotation tooling + -32602 unknown-tool rendering (#1632)

Card 9/10 of the SDK-v2 + New Spec stack. Adds SEP-2243 `x-mcp-header`
tooling to the Tools tab and adjusts result rendering for SDK v2's
unknown-tool behavior, and bumps the SDK to 2.0.0-beta.5.

- **`core/json/xMcpHeader.ts`**: a faithful port of the SDK's (non-public)
  `scanXMcpHeaderDeclarations` — validates every SEP-2243 constraint
  (primitive types incl. `number`, RFC 9110 token, case-insensitive
  uniqueness, statically-reachable `properties`-only chain) and derives the
  mirrored `Mcp-Param-{Name}` params for a kept tool.

- **Excluded tools**: the SDK silently drops tools with invalid annotations
  from `tools/list`. InspectorClient now re-lists the RAW (unfiltered) list
  and scans it to recompute the excluded set with reasons — gated to the
  modern non-stdio era (matching the SDK's gate), recomputed on every
  aggregate refresh, surfaced via a new `excludedToolsChange` event +
  `getExcludedTools()` and `useInspectorClient().excludedTools`. The Tools
  sidebar lists them struck-through under an "Excluded (SEP-2243)" divider
  with the reason on hover.

- **Mirrored args**: ToolDetailPanel shows a "Mirrored request headers"
  section listing each arg → `Mcp-Param-{Name}` (with the browser-omits note).

- **-32602 rendering**: SDK v2 rejects an unknown-tool call with `-32602`
  instead of an `isError` result, which previously rendered nothing. New
  ToolCallErrorPanel renders a thrown tool-call error, with a targeted
  "Unknown Tool" hint for `-32602`.

Adds unit + Storybook + integration coverage (a real modern server with an
invalid-header tool fixture) for every path. `npm run ci`-equivalent checks
green.

Closes #1632

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5

* test-servers: x-mcp-header modern showcase config + preset (#1632)

Register the `invalid_header_tool` fixture as a preset and add
`xmcpheader-modern-http.json` — a modern HTTP showcase serving echo, a
valid-header `get_weather`, and the invalid-header tool — so the Tools-tab
x-mcp-header tooling (mirrored headers, excluded-tools surface, -32602
rendering) can be exercised manually. Documented in the README alongside the
other per-SEP showcase configs. Smoke-tested end to end in the web client.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5

* docs: add #1632 proof screenshots to pr-screenshots

End-to-end web-client verification of the x-mcp-header Tools tooling against
the modern showcase server: excluded-tools surface + reason, mirrored-headers
section, and the -32602 unknown-tool error panel.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5

* Respond to review: disambiguate -32602, doc/search nits (#1632)

Addresses the review on PR #1735:

1. -32602 "Unknown Tool" mislabel: `-32602` is generic Invalid params, so a
   known tool called with bad arguments hit the same code and rendered under
   the definitive "Unknown Tool" heading. Replace `isUnknownToolError` with
   `classifyToolCallError(code, message)` → "unknown-tool" only when the
   message names it (not found / unknown tool / …), else "invalid-params"
   (new "Invalid Parameters" heading + schema hint), else "generic".

2. Double pagination walk: expand the `listAllTools` comment to document that
   the excluded-tools recompute is a deliberately un-cached second raw walk
   (doubles list round-trips on modern non-stdio, runs even on a cache-served
   aggregate) — accepted for a debugging tool, no-op on legacy/stdio.

3. Doc nit: `scanXMcpHeaderDeclarations` no longer claims to visit "$ref
   targets" — reworded to note `$ref` is never followed (refs resolve into the
   unreachable `$defs` body).

4. Search nit: the excluded-list filter now matches name AND title, matching
   the main tools filter.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5

* Tighten unknown-tool matcher to be tool-scoped (#1632 re-review nit)

The re-review noted the -32602 disambiguation was symmetric: broad markers
("does not exist", "unrecognized") could inversely mislabel a genuine
invalid-params message (e.g. "property 'region' does not exist") as "Unknown
Tool". Scope the not-found/does-not-exist family to require the word "tool" in
the same clause (matching the SDK's "Tool <name> not found"); keep "unknown
tool" / "no such tool" as standalone unambiguous markers. Adds a
tool-less-"does not exist" test asserting it classifies as invalid-params.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5

* Add trigger_invalid_params showcase tool + Invalid Parameters screenshot (#1632)

Adds a `trigger_invalid_params` spec-error trigger to the modern showcase
server: the injector answers its `tools/call` with a real `-32602 Invalid
params` JSON-RPC error whose message does NOT name an unknown tool. This lets
the "Invalid Parameters" branch of the tool-call error panel (added in the
review response) be exercised live, distinct from the unknown-tool `-32602`.

- `SPEC_ERROR_TRIGGERS` gains the `trigger_invalid_params` entry (HTTP 200,
  in-band -32602) + preset registration.
- Enable `injectSpecErrors` on `xmcpheader-modern-http.json` and add the tool.
- README + pr-screenshots README document it; adds the live "Invalid
  Parameters" proof screenshot.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 11:42:54 -04:00
Cliff Hall
2259b1a3e1
Tasks: io.modelcontextprotocol/tasks extension on modern servers (era fork) (#1731)
Some checks failed
Run install, format, lint, build, and test on every push / build (push) Has been cancelled
Run install, format, lint, build, and test on every push / publish (push) Has been cancelled
Run install, format, lint, build, and test on every push / publish-github-container-registry (push) Has been cancelled
* Tasks: modern io.modelcontextprotocol/tasks extension (era fork core)

Core + test-server implementation of the SEP-2663 tasks extension for modern
(2026-07-28) connections, forked from the legacy 2025-11-25 tasks path.

SDK v2 removed all tasks support AND era-gates the tasks/* spec methods out of
the modern era on both the client (outbound) and server (inbound) sides, and its
codec rejects a resultType:"task" result outright. So the Inspector drives the
extension itself: task creation rides tools/call (a modern method) with the
resultType:"task" frame rewritten at the transport to a CallToolResult carrying
the handle in _meta; tasks/get/update/cancel go over a raw-wire request channel
that bypasses the era gate (full modern envelope, string ids, consumed by the
transport). input_required tasks reuse the existing pending-request modal via
fulfilInputRequests, answered with tasks/update. No tasks/list, no tasks/result.

Tab + run-as-task gate on the negotiated extension; the managed task store's
refresh re-polls known handles on modern (no server list). Test server serves
the extension via an Express interceptor (SDK can't) + a shared poll-driven
runtime. Full integration coverage of both eras.

Refs #1631

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5

* Tasks modern era: tests + coverage (raw-wire, schemas, era integration)

Adds unit coverage for modernTaskSchemas and the raw-wire request channel
(transport-null guard, send failure, timeout, error response, teardown), plus
end-to-end era integration tests (create/poll/input-required/update/cancel,
synchronous passthrough, unknown-id error, modern store re-poll). Fixes the App
and constructor-capabilities tests for the always-on tasks extension.

Refs #1631

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5

* Tasks modern era: docs + share test servers per era to de-flake coverage

Document the tasks era-fork test configs in README and the new core/test-server
files in AGENTS.md. Share one modern + one legacy test server across the era
integration block (started lazily, stopped in afterAll) — starting a fresh
Express server per test made connection negotiation flaky under the heavy
concurrent coverage run.

Refs #1631

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5

* Restore pr-screenshots deleted inadvertently

The logging/subscriptions PR screenshots + README were staged-deleted in the
working tree before this branch; restore them from v2/main so this PR doesn't
remove other PRs' artifacts.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5

* Tasks modern era: offer Run-as-task for any tool + PR screenshots

On modern (SEP-2663) task creation is server-directed — the client declares the
extension once and any tool may return a task — so the Tools screen now offers
'Run as task' for every tool on a modern connection (threaded via a modernTasks
prop through InspectorView → ToolsScreen → ToolDetailPanel), rather than gating
on the legacy per-tool taskSupport. Verified end-to-end in the web UI (remote
proxy): create → poll tasks/get → input_required elicitation → tasks/update →
inline result, plus the unchanged legacy tasks/list/result flow. Adds proof
screenshots.

Refs #1631

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5

* Tasks modern era: address two follow-ups (task-input note + ordinary-path auto-poll)

1. The pending-request note now distinguishes a modern task round from an MRTR
   round: a new 'task-input-required' PendingRequestOrigin threads through
   fulfilInputRequests → the pending-request modal → MrtrOriginNote, which shows
   'submitted via a tasks/update request (SEP-2663), not a retry' instead of the
   MRTR retry copy.

2. The ordinary (non-run-as-task) modern callTool path now auto-polls an
   unsolicited task handle: attemptToolCall detects the rewritten handle in the
   result _meta and drives it to termination (get → input_required/update →
   inline result), dispatching task events so the Tasks tab + toasts track it and
   the tool result resolves to the task's final result. The poll loop is shared
   with the streaming path via extracted helpers (submitModernTaskInput /
   modernTaskTerminalOutcome / modernTaskPollInterval).

Refs #1631

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5

* Tasks modern era: address review (negotiated envelope version, input-round cap, clarifying comment) + fresh smoke screenshots

Review of #1731:
1. withModernTaskEnvelope now stamps the NEGOTIATED protocol version
   (getProtocolVersion() ?? MODERN_PROTOCOL_VERSION) so the envelope matches the
   MCP-Protocol-Version header instead of a hardcoded constant.
2. A never-completing input_required task is now bounded by MRTR_MAX_ROUNDS in the
   shared submitModernTaskInput (threaded through both poll loops), so a
   non-conformant server can't re-prompt the user forever. New modern_loop_task
   test-server tool + integration test cover the cap.
3. ToolDetailPanel: comment clarifying that per-tool taskSupport:"forbidden" is
   deliberately ignored on modern (task creation is server-directed under SEP-2663).

(The abort-during-poll observation is addressed in the PR reply: the cancel path
uses tasks/cancel and disconnect rejects the in-flight raw request, so the poll
stops; matches legacy.)

Fresh web-UI smoke screenshots (modern connected, task completed, input_required
with the corrected tasks/update note, input completed).

Refs #1631

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5

* Tasks modern era: fix uncancelable input-required loop + sticky cancel

Two bug fixes reported during manual testing of the tasks era-fork (#1631):

1. A modern task paused at `input_required` (e.g. `modern_loop_task`, which
   never advances) could not be cancelled: the poll loop blocked on the
   pending-request modal, so `cancelRequestorTask` sent `tasks/cancel` but the
   modal kept re-prompting. Now each modern poll loop registers a per-task
   AbortController (`registerTaskInputAbort`), `cancelRequestorTask` aborts it
   to reject the pending elicitation and unblock the poll, and the embedded
   input requests are tagged with their owning task id
   (`RELATED_TASK_META_KEY`) so the pending-request UI's Cancel action cancels
   the TASK rather than sending a "cancel" answer the server would re-prompt on.

2. On the legacy server, cancelling a task briefly showed CANCELLED then
   flipped to COMPLETED when the in-flight tool call resolved. Cancellation is
   a deliberate terminal decision, so `ManagedRequestorTasksState` now remembers
   cancelled task ids and ignores any later non-cancelled status update.

Also: move the compact task id onto its own line in the embedded monitoring
sidebar so the status badge / Cancel Task control don't wrap; DRY the duplicated
task poll-interval logic into a shared `taskPollInterval` helper.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5

* Refresh tasks PR screenshots + capture both bug-fix scenarios

Recapture the five tasks flow screenshots against the current build and add
two demonstrating the post-review fixes: modern_loop_task cancelled from the
pending-request modal (uncancelable-loop fix) and a legacy task staying
CANCELLED after the in-flight call resolves (sticky-cancel fix).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5

* Address re-review: sticky cancel on legacy refresh + App cancel-routing test

Re-review of the cancelable-loop / sticky-cancel fix raised two non-blocking
hardening items:

1. The legacy tasks/list refresh() rebuilt the list from the server, honoring
   only dismissedTaskIds — so a cooperative-cancel server that re-lists a
   user-cancelled task as completed could un-stick the cancel on a manual
   Refresh. The refresh path now pins a cancelledTaskIds task to "cancelled"
   (keeping it visible-as-cancelled, matching the live-merge handlers) rather
   than dropping it. Covered by a new state test.

2. Added an App.test.tsx case locking in the user-facing half of the fix: Cancel
   on a task-input-required elicitation calls cancelRequestorTask (not respond).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 19:17:04 -04:00
cliffhall
19a6df7ad3 SDK v2 migration (behavior-neutral, versionNegotiation: 'legacy') (#1624)
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>
2026-07-15 10:44:24 -04:00
Cliff Hall
3d75ef5f8e
web: Accessibility — accessible palette + WCAG AA contrast, labels, names, roles (#1679)
* web: accessible light/dark color palette + dark-mode legibility fixes (#1678)

Ports the design system's accessible palette. Colors are defined once as Mantine
color tuples in theme.ts (shade 0 lightest → 9 darkest); every `--inspector-*`
token in App.css references `--mantine-color-*`, so improving the tuples cascades
through the whole UI.

- theme.ts: full accessible tuple set (inspector/gray/dark/green/red/yellow/blue/
  teal/violet), WCAG-AA at the primary shades. Tuples are the one place literal
  hex belongs — the token source. Primary stays inspector-7 (light) / -8 (dark).
- theme/ActionIcon.ts: default `color: "gray"` so chrome icons (settings, theme
  toggle, sidebar, pin, sort, replay) render neutral grey instead of inheriting
  the blue primary. Icons that set their own color/variant still override.
- App.css: fix light surfaces (`surface-body` gray-0, `surface-card` white — the
  body token pointed at near-black dark-9); add direction/method badge tokens;
  lift the semantic status/log/task/history colors 1–2 steps in the dark scheme
  (the shared `*-6` shade sat too dark on charcoal). Light `:root` keeps `*-6`.
- Badges: MessageDirectionBadge + new MethodBadge element driven by
  `--inspector-badge-*` tokens (deep fill + light text in dark, tinted in light)
  via Mantine `bg`/`c` style props with `autoContrast={false}`; swaps
  `<Badge color="dark">{method}</Badge>` in ProtocolEntry/NetworkEntry (their
  Badge import stays — both still use it for status/category chips). Stories +
  test added for MethodBadge (≥90 coverage gate).

Domain badge semantics live in the element components, not theme files. No raw
hex outside the tuples; no inline styles. Supersedes the reverted #1677.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BrihGWcrM9JGRyu41nzZYw

* web: WCAG AA accessibility pass — contrast, labels, names, roles (#1678)

Drives the axe-core ruleset (the engine behind Storybook's a11y addon) to
zero violations across all stories in light and dark mode.

Contrast (805 → 0 hits):
- Darken Mantine `*-light-color` + `dimmed` in light mode (and lift dark
  `dimmed`) via `!important` App.css overrides — Mantine injects these at
  runtime at matching specificity, so specificity alone loses the tie.
- `filledBadgeColor` pins amber (orange/yellow) filled badges to shade 5 so
  `autoContrast` (JS-computed from the theme shade, not CSS vars) picks black.
- `accessibleTextColor` routes colored log/filter text through the scheme-aware
  `-light-color` token so it clears AA on light and dark surfaces.
- `status` Badge variant: amber statuses use black-on-amber (orange text can't
  reach AA on light at any Mantine shade).
- Resource URIs use `-light-color` (blue-4 was 4.38:1 in dark).

Names / roles:
- `aria-dialog-name`: custom-header modals use `Modal.Title` (wires the
  dialog's `aria-labelledby`) instead of a bare `Title` + a root `aria-label`
  (which also tripped `aria-prohibited-attr`).
- `button-name`: a `Modal` theme default labels every built-in close button.
- `label`: `aria-label` on the previously-unlabelled Textarea/Select inputs.
- `scrollable-region-focusable`: keyboard-focusable content Code/ScrollArea
  viewports; long OAuth values wrap instead of scrolling.
- `heading-order`: correct heading levels (keeping visual size via `size`) so
  outlines don't skip a level under the h2 modal/section titles.

Scrollbars: `type="scroll"` ScrollArea theme default — scrollbars auto-hide and
show only while scrolling (a deliberate product choice over the always-visible
a11y default).

`region` is disabled in the Storybook a11y config: isolated component/screen
stories render outside the app's `AppShell` landmarks, so it's a page-level
concern the components can't own.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* web: let the resource template form fill the main-area width (#1678)

Drop the maw=40% cap on the template-form pane so the URI preview and
variable inputs use the full width beside the sidebar, matching the
resource preview pane.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* web: left-align + reverse the input-panel action rows (#1678)

Move the footer action buttons to the left (justify flex-start) and put
the primary action first on the Tool / Prompt / Resource-preview /
Resource-template panels — Subscribe, Refresh, View Source on the
preview panel; Execute before Cancel on the tool panel. Shortens the
pointer travel from the sidebar controls / the form fields above to the
actions, which matters most when the monitoring sidebar is hidden and
the form is wide.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* web: match the dark-mode body background to the header tab bar (#1678)

Set the dark `--inspector-surface-body` to `dark-8` (the Mantine
SegmentedControl fill used by the header tab bar) instead of `dark-9`,
mirroring light mode where the body and the tab bar share `gray-1`.
Cards (`gray-9`) still sit a step above the body, and the contrast audit
stays at zero AA violations on the slightly lighter background.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* web: enforce the a11y gate + fix modal-overlay heading-order (review #1679)

Address the code review:
- Flip the Storybook a11y addon from test: 'todo' to 'error' so axe
  violations fail the play-function tests in CI — the zero-violation
  state is now gated, not point-in-time.
- Enforcing it surfaced a real heading-order skip: the Tools screen's
  h4 'Tools'/'Results' sit under an h2 Modal.Title when a sampling/
  elicitation request modal opens over them. Lift both to h3 (size h4
  preserves the look) so h2→h3 is valid in any DOM order.
- Comment the intentional color+c pairing on the Remove/Disconnect
  danger buttons.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* web: brighter warning alerts + pin the elicitation form's actions (#1678)

- Add a 'warning' Alert variant: a bright light-yellow fill (yellow-1)
  with crisp near-black text and a bright yellow-5 left accent, instead
  of the muddy brown a tinted yellow alert produced once
  yellow-light-color was darkened to yellow-9 for contrast. Dark mode
  keeps the light-on-dark-tint treatment. Applied to all five yellow
  warning/notice alerts.
- Elicitation form: scroll only the form fields (capped ScrollArea),
  keeping the quoted message, warning, and Cancel/Decline/Submit buttons
  in view. The modal has no other close affordance, so the actions must
  never scroll out of reach.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* web: robustly pin the elicitation form's actions (#1678)

The previous approach capped only the fields' ScrollArea with a fixed
rem reserve for the pinned chrome, which underestimated it on some
viewports — the whole modal then overflowed and the buttons clipped
half-off-screen. Instead cap the whole panel below the modal height and
lay it out as a flex column: the message and the warning + buttons are
pinned (flex: 0 0 auto) and only the fields (flex: 1) shrink and scroll.
Verified the buttons stay within the panel and the modal fits the
viewport down to very short heights.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* web: fix elicitation form scrolling — fields scroll, actions pinned (#1678)

The prior fix capped the panel with a bare max-height, but a flex:1
ScrollArea's viewport won't bound against a max-height (not fixed-height)
parent, so a tall form's fields overflowed unscrollably (clipped) rather
than scrolling. Adopt ToolDetailPanel's pattern instead: the modal gives
its body a bounded flex column, the panel fills it, and the fields use a
flex: "0 1 auto" ScrollArea — which sizes to content when the form is
short (modal stays compact, nothing scrolls) and shrinks + scrolls once
the panel is capped at the modal height, keeping the message, warning,
and Cancel/Decline/Submit pinned in view. Add a ManyFields story (with a
bounded-height decorator mirroring the modal) to cover the scroll case.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* web: fix elicitation scrolling in the real modal (cap fields, not flex)

The flex-column approach (panel flex:1 in a flex-column modal body, fields
flex:"0 1 auto") worked in a fixed-height Storybook decorator but NOT in the
real modal: the modal body is bounded by max-height, not an explicit height,
so the fields' ScrollArea viewport still couldn't bound — a tall form's
fields overflowed and pushed the warning + Cancel/Decline/Submit off-screen.

Drop the flex plumbing (and the modal-body styles) and cap the fields
directly with ScrollArea.Autosize mah=calc(90dvh - 22rem): it sizes to
content when the form is short (modal compact, nothing scrolls) and scrolls
once the fields hit the cap. The 22rem reserve = the measured non-field
chrome (modal header/padding + message + warning + buttons + gaps), so the
whole modal stays within 90dvh and the actions are always in view. Verified
in the real PendingClientRequestModal (new ElicitationFormTall story) for
both short and tall forms.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* web: pin the sampling modal's actions too (#1678)

Apply the elicitation-form treatment to SamplingRequestPanel: wrap the
request/response content in a ScrollArea.Autosize capped at
calc(90dvh - 10rem) so it scrolls while the Reject / Send Response buttons
stay pinned in view (the modal has no other close affordance). The 10rem
reserve is the pinned chrome (modal header/padding + the actions + gaps) —
smaller than the elicitation form's 22rem since there's no warning banner.
Add a SamplingTall story (8 messages + preferences) to cover the scroll
case in the real PendingClientRequestModal.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 18:20:55 -04:00
Cliff Hall
92da6c1675
fix(web): collapsible tool description in ToolDetailPanel — shown by default (#1381) (#1459)
* fix(web): collapse long tool descriptions in ToolDetailPanel (#1381)

Long tool descriptions (e.g. sequential-thinking-server's "Sequential
Thinking" tool) filled the panel and pushed the form + Execute button
off-screen. The internal scroll region for the form/progress was already
in place; this adds the collapsible description.

- Wrap the description in a Mantine Collapse, collapsed by default.
- Expose the trigger as a chevron ActionIcon pinned to the right of the
  tool-name row (FaChevronRight collapsed / FaChevronDown expanded), with
  Show/Hide description aria-labels. Rendered only when a description exists.
- Reset to collapsed on tool change via the adjust-state-during-render
  pattern, mirroring how ToolsScreen clears formValues.

Tests + stories cover collapsed default, expand/collapse toggle, reset on
tool switch, and the no-description case.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(web): show tool description by default, click to hide (#1381)

Most tool descriptions are short, so default to showing the description
and let the user collapse a long one via the chevron, rather than the
reverse. Flips the initial/reset state to open and updates the toggle
aria-labels, tests, and stories accordingly.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(web): use Ri disclosure chevrons in ToolDetailPanel (#1459 review)

Swap react-icons/fa FaChevronDown/FaChevronRight for the established
react-icons/ri RiArrowDownSLine/RiArrowRightSLine disclosure-chevron
convention already used in ResourceLink, keeping chevron visuals
consistent across the app.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* a11y(web): wire description toggle as expandable control (#1459 review)

Add aria-expanded + aria-controls to the description chevron, tying it to
the Collapse region (via useId) so assistive tech announces it as a single
expandable control rather than two differently-labelled buttons. Adds a
test covering the wiring and the aria-expanded flip.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 19:13:29 -04:00
Cliff Hall
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.
2026-06-04 08:49:47 -04:00
Cliff Hall
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>
2026-06-02 17:52:20 -04:00
Cliff Hall
fc41fdc10a
feat(apps): render tool.icons on ToolListItem and ToolDetailPanel (#1264) (#1293)
* feat(apps): render tool.icons on ToolListItem (#1264)

Add icon rendering to ToolListItem so the Tools sidebar matches the
Apps sidebar treatment. Falls back gracefully when tool.icons is absent.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(stories): use #228be6 in ToolListItem WithIcon data URL

The source SVG had %23228be6 pre-encoded, but encodeURIComponent then
re-escaped the % to %25, leaving fill="%23228be6" in the decoded SVG
and making the icon render black instead of Mantine blue.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(apps): render tool.icons on ToolDetailPanel (#1264)

Show the tool icon next to the title in the detail panel, matching the
header icon size used in AppsScreen (24x24, fit: contain). Adds a
WithIcon story and tests for icon-present/icon-absent states.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 15:45:53 -04:00
Cliff Hall
1ffcdc38ca
feat(apps): add AppListItem and AppDetailPanel groups (#1261) (#1273)
* 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>
2026-05-04 10:28:58 -04:00
cliffhall
16f4d9b50f Address PR #1212 code review feedback
- Fix hasAnyAnnotation checking annotations.title which is never rendered
- Reset formValues when switching tools via onSelectTool handler
- Extract ToolProgress type from ToolDetailPanel, reuse in ToolsScreen
- Fix contradictory readOnlyHint + destructiveHint in story fixture
- Replace listMaxHeight function with LIST_MAX_HEIGHT constant

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-18 17:12:02 -04:00
cliffhall
92ecfcbd6c Phase 2: Refactor Tools cluster to use MCP SDK types
Replace ad hoc local types with MCP SDK schema types across the Tools
group components and ToolsScreen, per the v2 UX interfaces plan.

- ToolListItem: accept `tool: Tool` instead of flat `name`/`title`
- ToolControls: accept `Tool[]` + `selectedName` instead of `ToolListItemProps[]`
- ToolDetailPanel: accept `tool: Tool` instead of flat props; use SDK
  `ToolAnnotations` (readOnlyHint/destructiveHint/idempotentHint/openWorldHint);
  use `ProgressNotification['params']` for progress
- ToolResultPanel: accept `CallToolResult` instead of `ResultContentItem[]`
- ToolsScreen: accept `Tool[]` + `selectedToolName` + `ToolCallState`;
  derive selected tool via `tools.find()`; own `formValues` state locally
- Delete local `ToolAnnotations`, `ResultContentItem` types
- Update all stories with real SDK `Tool` fixtures
- Update ConnectedView ToolsActive story

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-18 16:57:10 -04:00
cliffhall
3e2e44ccfc Address PR #1211 code review feedback
1. ServerCard: import ConnectionStatus and ServerType from core instead
   of inline union types
2. ResourcePreviewPanel: move ContentBlock import to top of file
3. ContentViewer: remove dead annotation mimeType check in isJsonText
4. ProgressDisplay: use Pick to exclude progressToken from params type;
   remove fake tokens from callers and stories
5. AnnotationBadge: restore human-readable priority labels (high/medium/low)
   and remove unused --inspector-annotation-* CSS vars from App.css
6. MessageBubble: add fallback rendering for resource, resource_link,
   tool_use, and tool_result content block types
7. TaskControls: import TaskStatus directly from SDK instead of via TaskCard

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-18 11:48:21 -04:00
cliffhall
2d4fca4fa6 Phase 1: Refactor all 16 element component interfaces to use MCP SDK types
Replace ad-hoc local type definitions in element components with real
MCP SDK types and Inspector wrapper types from core/mcp/types.ts:

Schema-typed primitives:
- LogLevelBadge: LogLevel → LoggingLevel (SDK)
- TransportBadge: "stdio"|"http" → ServerType (core), add sse/streamable-http
- TaskStatusBadge: local TaskStatus → SDK TaskStatus (working/input_required/...)
- ServerStatusIndicator: inline union → ConnectionStatus (core), "failed"→"error"

MCP wrapper consumers:
- AnnotationBadge: label+variant → facet+value with SDK Role type
- CapabilityItem: name:string → capability:keyof ServerCapabilities|ClientCapabilities
- LogEntry: flat props → entry:{receivedAt:Date, params:LoggingMessageNotification["params"]}
- MessageBubble: flat props → message:SamplingMessage|PromptMessage
- ProgressDisplay: flat props → params:ProgressNotification["params"]

Composite-input elements:
- ContentViewer: type+content+mimeType → block:ContentBlock (SDK discriminated union)
- InlineError: message+details → error:{message,data?}
- ConnectionToggle: checked+loading+onChange → status:ConnectionStatus+onConnect/onDisconnect

Also updates all group/screen/view consumers to keep the build green,
and renames CSS variable --inspector-status-failed → --inspector-status-error.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-17 11:57:53 -04:00
cliffhall
8e73bc6ffb Refactor ServerCard with active server logic, rename StatusIndicator, and polish UI panels
- Rename StatusIndicator to ServerStatusIndicator across all files
- ServerCard: add activeServer/onSetActiveServer props for coordinated
  connection flow — when a card's toggle is switched on, it sets itself
  as the active server, shows "Connecting" status, and dims all other
  cards (opacity + pointerEvents none + inert for full keyboard blocking)
- ServerListScreen: manage activeServer state internally, pass to cards;
  switch to full-width layout with 3-column grid and scrollable card area
- ServerCard: move "Test Client Features" dropdown above ContentViewer
  with min-height row for consistent card heights
- ToolResultPanel: move Clear button to top-right header row
- ToolDetailPanel: right-align Execute Tool button, size md, not full-width
- ListChangedIndicator: bump Refresh button to size sm
- ToolsActive story: expand to 30 tools with titles
- UnconnectedView stories: all servers disconnected, remove Connecting story

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 18:00:18 -04:00
cliffhall
dd46acba87 Refactor renamed the component groups to something less obtuse:
Elements -> Groups -> Screens -> Views
2026-03-28 12:58:47 -04:00
Renamed from clients/web/src/components/molecules/ToolDetailPanel/ToolDetailPanel.tsx (Previous history)