Skip to content

Use the Ace JSON editor everywhere JSON is edited, and restore the v1 raw-JSON toggle on tool forms #2151

Description

@cliffhall

Summary

#2094 introduced JsonObjectInput — an Ace-backed JSON editor (syntax highlighting, line numbers, folding, brace auto-closing, per-line error annotation from Ace's JSON worker) — and wired it into one place: Server Settings → Request Metadata.

Everywhere else the user hand-writes JSON, they are still doing it in a bare box. ace-builds and react-ace already ship in clients/web, so the cost of the remaining sites is wiring and generalization, not a new dependency.

Two things are wanted:

  1. Use the Ace editor wherever JSON is typed today, in place of the plain Textarea / Mantine JsonInput.
  2. Restore the v1 raw-JSON toggle: even a schema the form can render should offer a switch to edit the whole arguments object as JSON directly.

Where JSON is edited or displayed today

1. SchemaJsonField — the "can't render this" fallback (SchemaForm.tsx)

A Mantine JsonInput (formatOnBlur, autosize) is what an object-, array-, or union-typed argument falls through to. It is the single highest-value swap, because four surfaces share it through SchemaForm:

Consumer What it edits
ToolDetailPanel tool arguments
AppDetailPanel MCP App tool arguments
ElicitationFormPanel an elicitation's form fields
InlineElicitationRequest the same, inline

This box also has the worst history in the repo — #1853, #1856, #1928, #2026 were all about typing in it. It is where the Ace affordances (a located parse error rather than one message for the whole document, folding a nested payload, auto-closed braces) pay off most.

2. A raw-JSON toggle on the whole arguments object (v1 parity)

v1 let you flip a rendered form over to editing the arguments object as JSON. v2 has no such escape hatch, so a value the widgets can't express — or a payload you want to paste in whole — has no route in. Add the toggle at the SchemaForm level so all four consumers above inherit it.

Two existing behaviors it has to respect:

3. ImportServerJsonPanel — "File Contents"

A monospace Textarea holding a pasted or uploaded client config / registry server.json (reached via ServerImportJsonModal). Ace's gutter annotation is directly useful here: the panel already validates the document and reports why it failed, but cannot say where.

4. ExperimentalFeaturesPanel — the raw JSON-RPC "Request" box

A monospace Textarea the user types a whole JSON-RPC request into. This component is not wired into the app yet (it has tests and stories only), so the ask is that it lands on Ace rather than being migrated later.

5. Read-only JSON display — inside ContentViewer

Every read-only JSON payload in the app funnels through one element,
ContentViewer, which dispatches per MIME kind and falls back to a
looksLikeJson heuristic for untyped text. So this is a change to that one
JSON branch, and its eleven consumers inherit it: ProtocolEntry,
NetworkEntry, ToolResultPanel, StructuredOutputPanel,
ResourcePreviewPanel, ResourceLink, ServerCard, TaskCard,
ConnectionInfoContent, MessageBubble, AppsScreen.

Ace replaces the JSON branch only. Markdown, XML, CSS, YAML, CSV, HTML,
PDF and images keep the renderers they have — the user's framing exactly: not a
replacement for ContentViewer, a renderer it selects when the data is JSON.

Three things to get right:

  • The copy button is already there and must stay. ContentViewer overlays
    it (CopyOverlay) above whichever renderer it picked, so copyable keeps
    working across the swap rather than needing to be re-added.
  • Colorization is not the win — folding is. JSON already highlights today
    via CodeHighlight (lazily-imported Prism). What Ace adds is code folding,
    line numbers and a gutter on a large payload. Decision to record on the
    PR:
    does the Ace read-only mode replace the CodeHighlight JSON path
    (letting the Prism json grammar loader be dropped), or do both stacks stay?
    Two highlighters for one language is the kind of thing that rots.
  • wrap={false} callers must not get it. ServerCard passes it for a
    fixed-height, single-line, ellipsis-clipped box, and pretty-printed JSON
    collapsed onto one line is exactly what that prop is documented not to be
    used for. That path keeps the current renderer.

6. Edit and replay (Protocol tab)

Replay is one-click today: ProtocolEntry shows a ReplayButton when
isReplayableProtocolMethod(method), and App.tsx calls
replayProtocolRequest(inspectorClient, method, params, tools) with the
entry's params verbatim.

Add an Edit and replay action beside it, gated on the same predicate, that
opens a modal with the request params in an editable Ace editor (seeded from
the entry), plus Send and Cancel. Send dispatches through the existing
replayProtocolRequest with the edited params — same code path, same
Can't replay failure toast — so this is a new way to supply params, not a
second replay implementation.

Invalid JSON disables Send rather than sending the last valid value: unlike the
Server Settings metadata box this modal has a commit gesture to gate, which
is the same reasoning SchemaJsonField uses.

Explicit non-goals

These look adjacent and are not JSON:

  • Key/value row editors that really are string→string: custom headers, stdio env, OAuth authorization params.
  • ServerConfigModal's Arguments (one per line) and Environment (KEY=VALUE per line) textareas.
  • SamplingRequestPanel's Response box, which is the sampled message text.
  • The CLI and TUI. Ace is a browser editor; the terminal clients keep their own input.
  • Non-JSON ContentViewer kinds — markdown, XML, CSS, YAML, CSV, HTML, PDF, images. Only the JSON branch changes.

Generalization needed

JsonObjectInput cannot be reused as-is by SchemaForm — the two want different contracts, deliberately:

JsonObjectInput (#2094) SchemaJsonField
Value type StrictJsonObject (object only) any JsonValue — arrays and scalars included
While the draft is invalid parent is not told; last valid object stands (no Save button to gate) parent is told undefined, and invalidity is reported up via onValidityChange

So this needs a generalized element — either widened props on JsonObjectInput or a shared Ace-based base with two thin wrappers — rather than a drop-in swap. Both already carry the same draft/value split (typed text is the display source, the parent only ever sees parsed JSON); that part is shared and should end up written once.

Also carry forward from #2094:

  • Ace's JSON worker URL registration is module-scoped and idempotent — it must not move into a per-mount effect.
  • Ace fires two change events for a replace (remove, then insert), so select-all-and-retype passes through a momentarily empty document. JsonObjectInput coalesces the pair in a microtask and tracks what it last emitted; any new consumer inherits that hazard.
  • ariaLabel is load-bearing — Ace names its hidden textarea "Cursor at row N", which is a position readout, not a name.

Acceptance

  • A shared Ace-based JSON editor element that serves both the object-only and any-JSON-value contracts, plus a read-only mode.
  • SchemaJsonField renders it, so all four SchemaForm consumers get it.
  • A raw-JSON toggle on SchemaForm for the whole arguments object, honoring SchemaForm: an invalid JSON draft in an optional field is silently dropped from the submission #2020 draft validity and Tool schemas with a root-level anyOf / oneOf render no input fields #2123 variant pruning.
  • ImportServerJsonPanel renders it.
  • ExperimentalFeaturesPanel's Request box renders it.
  • ContentViewer's JSON branch renders it read-only, keeping the copy overlay and leaving wrap={false} callers alone; the CodeHighlight-vs-Ace decision is stated on the PR.
  • An Edit and replay action in ProtocolEntry opening a modal (Ace editor + Send + Cancel) that dispatches through the existing replayProtocolRequest, with Send disabled on invalid JSON.
  • Storybook stories and play functions for the new/changed surfaces; >=90% per-file coverage.
  • Before/after screenshots on the PR.

Sequencing

Six surfaces is a lot for one PR. The shared element comes first; after that the
consumers are independent and can land separately, with the SchemaForm pair
(items 1 + 2) as the highest-value slice and ContentViewer (item 5) as the
widest-blast-radius one.

Metadata

Metadata

Assignees

Labels

enhancementNew feature requestv2Issues and PRs for v2

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions