Commit graph inspector/core/mcp/state/resourceSubscriptionsState.ts
Author SHA1 Message Date
Cliff Hall
373f346e5f
chore: close quality-gate coverage gaps (#1689) (#1766)
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
Closes #1689. Typecheck cli/tui src, add a core/ format+lint gate, and gate core/json + core/client coverage. See PR #1766 for the full review history.
2026-07-24 17:20:12 -04:00
Cliff Hall
320daf2f67
Resources: subscriptions/listen model on modern servers (era fork) (#1630) (#1730)
Fork resource subscriptions by protocol era: legacy keeps resources/subscribe; modern (2026-07-28) models subscriptions as a filter over one long-lived subscriptions/listen stream with reconnect-by-re-listen (capped backoff), an optimistic Connecting→Listening header badge, and a fix so the browser proxy no longer blocks listen() ~60s. Closes #1630.
2026-07-20 19:33:40 -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
dc31c28102
fix(core): make "error" the canonical terminal status on a mid-session crash (#1490) (#1493)
* fix(core): make "error" the canonical terminal status on a mid-session crash (#1490)

On a real mid-session transport crash many SDK transports fire BOTH
`onclose` and `onerror` in a transport-dependent order. The `onclose`
guard (`if status !== "disconnected"`) let a trailing `onclose`
downgrade a crash's `"error"` status back to `"disconnected"`, so the
persistent terminal status differed by ordering:

- onerror-then-onclose → "disconnected"
- onclose-then-onerror → "error"

`onclose` now treats `"error"` as terminal (only `"connected"`/
`"connecting"` → `"disconnected"`), so `"error"` is the canonical resting
status in both orderings. It still emits the `disconnect` event exactly
once so session-teardown consumers fire identically either way.

Because the state managers gated list/message/subscription/task teardown
on `getStatus() === "disconnected"`, holding the status at `"error"`
would have left them stale in the onerror-first ordering — swapping one
ordering-dependence for another. Introduce `isTerminalStatus()`
(disconnected || error) and key every teardown guard off it so they
clear identically regardless of crash ordering.

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

* fix(core): single disconnect event when explicitly disconnecting from "error" (#1490 re-review)

Addresses the non-blocking observation from the PR review: explicitly
calling disconnect() from a held-"error" status could fire the
`disconnect` event twice. close() can trigger the transport's onclose
synchronously; onclose (holding status at "error") fired `disconnect`,
then disconnect()'s own guard saw status still != "disconnected" and
fired it again — a timing-dependence on whether the SDK closes sync.

Add a `disconnecting` ownership flag: onclose defers its status set +
`disconnect` event to disconnect() while it is set, so the
explicit-disconnect path emits exactly one `disconnect` regardless of
sync/async close. Crash-path onclose (flag unset) is unchanged.

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

* test(core): deterministically pin the sync-close disconnect double-fire (#1490 re-review)

The real-stdio test asserts the single-disconnect invariant but may not
exercise the sync-close branch (the one that double-fired) if the SDK
stdio transport closes asynchronously. Add a test that overrides the SDK
client's close() to invoke the transport's onclose synchronously,
forcing the exact timing the `disconnecting` flag guards. Verified it
asserts 2 (fails) without the flag and 1 (passes) with it.

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-15 10:15:29 -04:00
Cliff Hall
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>
2026-05-19 20:15:51 -04:00