Skip to content

feat(mcpi): experimental session CLI client (#1432) - #1783

Open
BobDickinson wants to merge 25 commits into
v2/mainfrom
v2/mcpi-client
Open

BobDickinson wants to merge 25 commits into
v2/mainfrom
v2/mcpi-client

Conversation

@BobDickinson

@BobDickinson BobDickinson commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

Closes #1432

Depends on #1782 (CLI improvements — handlers and shared helpers live there).

Summary

  • Add experimental clients/mcpi session CLI (connect once, many commands) with an implicit local Unix-socket daemon
  • Reuse clients/cli handlers / OAuth helpers via a temporary build-time @inspector/cli alias
  • Wire mcpi into monorepo validate / build / coverage / install-clients and document in AGENTS + specification/v2_cli_v2.md

Packaging

  • Not in the published @modelcontextprotocol/inspector tarball (files allowlist unchanged)
  • No root bin.mcpi — install via npm link from clients/mcpi for local use (see clients/mcpi/README.md)

Test plan

Made with Cursor

BobDickinson and others added 2 commits July 25, 2026 16:38
Add servers/list and servers/show, --relogin / --stored-auth-only, browser
OAuth navigation with OSC 8 links, and extract method handlers under
clients/cli/src/handlers. Tracks #1781.

Co-authored-by: Cursor <cursoragent@cursor.com>
Introduce clients/mcpi (session front-end + local daemon), wire it into
monorepo validate/build/coverage/install, and document the design. Not
shipped in the published inspector tarball. Closes #1432.

Co-authored-by: Cursor <cursoragent@cursor.com>
@BobDickinson BobDickinson added the v2 Issues and PRs for v2 label Jul 25, 2026
Base automatically changed from v2/cli-improvements to v2/main July 26, 2026 20:55
BobDickinson and others added 2 commits July 26, 2026 14:10
Take the merged #1782 CLI sources from v2/main; keep mcpi client wiring
in root validate/format/coverage; register mcpi in format-coverage verify.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@cliffhall cliffhall linked an issue Aug 17, 2026 that may be closed by this pull request
BobDickinson and others added 20 commits September 13, 2026 21:49
# Conflicts:
#	AGENTS.md
#	README.md
#	clients/launcher/package-lock.json
#	package.json
Two interface-drift breaks surfaced only at build/runtime (the git
merge itself was clean):

- clients/cli/handlers/method-types.ts dropped metaValueToString when
  _meta was widened to RequestMetadata (JsonValue, not string) in
  #1910. mcpi's stringifyMeta was flattening to strings for a type
  that no longer exists; pass the already-JSON metadata through
  directly instead.
- core/ gained proper-lockfile (auth/node/file-lock.ts) and yaml
  (mcp/skillFile.ts) as new dependencies. Both are bundled into mcpi's
  daemon via the noExternal @inspector/core reach-in, and both do a
  dynamic require() of a Node builtin in their CJS internals, which
  esbuild's ESM bundle output can't satisfy — the daemon threw on
  startup with 'Dynamic require of "process"/"path" is not
  supported' and every session-CLI test that needed a live daemon
  timed out waiting for it. Externalize both packages (already root
  dependencies, resolvable from node_modules at runtime).

Verified: npm run build:mcpi succeeds, npm run validate:mcpi (lint +
typecheck + 117 tests, coverage gate) passes clean.
Ad-hoc connect targets (bare URL/stdio, no catalog entry) had no way to
request auto/modern era negotiation - loadServerEntries/
headersToServerSettings never populate protocolEra, so an ad-hoc
connect always defaulted to legacy with no override, unlike a catalog
entry with a protocolEra field.

Add --era <legacy|auto|modern> to mcpi connect, applied via a new
withEraOverride() helper (mirrors the existing withConnectTimeout()
pattern) that overrides protocolEra on the resolved settings, or
synthesizes a bare-defaults settings object carrying just the override
when the target had none.
mcpi initialize never sent a live initialize request - it replayed
InspectorClient's cached connect-time state, which is populated the
same way regardless of era (legacy initialize response vs. modern
server/discover). The name was misleading, and it omitted the two
fields that do differ by era: protocolEra and (when probed)
supportedVersions.

Remove "initialize" from SESSION_RPC_METHODS (kept in
ONE_SHOT_METHODS - the one-shot CLI's --method initialize still
matches the literal wire method name on purpose). Add
mcpi sessions/show [session], combining daemon session bookkeeping
(name, serverIdentity, timestamps, isMru) with live connection state
(serverInfo, protocolVersion, protocolEra, capabilities, instructions,
supportedVersions). New daemon op sessions/show plus
SessionRegistry.sessionFor() backing both it and the existing
clientFor().

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CI (npm run validate) failed on verify:typecheck-coverage: mcpi had no
typecheck script wired into validate, unlike cli/tui/launcher. Fixing
that surfaced further gaps once mcpi's own toolchain was actually
exercised end-to-end:

- clients/mcpi: add typecheck/check scripts (validate -> check ->
  typecheck), tsconfig.test.json (mirrors cli's pattern), and the same
  compilerOptions overrides cli/tui use so core/'s
  noUncheckedIndexedAccess strictness doesn't spuriously fail here.
- clients/mcpi/package.json: bump @modelcontextprotocol/client, core,
  server, and server-legacy from a stale 2.0.0-beta.5 to 2.0.0 (real
  SDK version skew vs. the rest of the monorepo, caught by
  verify:dep-lockstep). Per AGENTS.md, the shared toolchain (eslint,
  vitest, typescript, etc.) is declared once at the repo root and in no
  client manifest, so drop mcpi's own copies of all of it instead of
  keeping them in sync by hand - matching cli/tui/launcher exactly, it
  now resolves the root copies via npm/Node's directory walk-up. Keep
  only what's genuinely client-specific: tsup (its bundler) and
  @types/express (needed transitively by the test-server barrel
  import, same reason cli has it). That transitive import also drags
  in a hoisted @types/node that drifts from root's, so pin it via
  overrides the same way clients/cli does.
- Fix real bugs typecheck caught: an unsound cast in format-session.ts,
  wrong-arity expectCliFailure() calls and an invalid
  AuthChallengeReason literal in two test files, and two throws missing
  an Error cause (preserve-caught-error) once the root's current eslint
  actually ran against this client for the first time.
- Register mcpi with the repo's cross-cutting guards that assumed a
  fixed client list: verify-test-timeouts.mjs (CONFIG_ROOTS /
  EXPECTED_PROJECTS plus its own test fixtures), verify-bundle-
  externals.mjs (BUNDLED_CLIENTS, since mcpi ships a tsup bundle),
  workflow-gate.test.mjs (local:validate must reach mcpi the same way
  it reaches its siblings), and root package.json's local:validate
  script.

Verified: npm run validate passes clean end-to-end (guards, core, and
all six clients).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…d them

The previous commit fixed verify:typecheck-coverage, which unblocked two
CI jobs that had never run to completion on this branch before: the
per-file branch-coverage gate and the externalized-dependency check.

Coverage (clients/mcpi, >=90% branches per file):
- server.ts, format-human.ts, and mcp.ts had branch coverage below the
  90% gate. The gaps traced to two features added this session that had
  thin or subprocess-only test coverage: sessions/show's enrichment
  fields (era, serverInfo, capabilities, supportedVersions, instructions)
  and the --era flag (previously untested at all).
- Added in-process tests (server.handle()/callDaemon() against a daemon
  constructed directly in the test file) instead of runMcp() subprocess
  calls, since subprocess code isn't visible to coverage instrumentation.
  Covers sessions/show's absent-field fallback paths and full success
  path, the --era auto/invalid-value paths, and the mcp.ts positional
  sessionArg fallback for sessions/show.

Build gate (verify:bundle-externals):
- mcpi's tsup entry is named mcp-bin.js (multi-entry config), not
  index.js like web/cli/tui, so the script's hardcoded index.js
  existence check reported "build/index.js is missing" for mcpi even
  after a real build. Added a per-client `entry` override (defaults to
  index.js) so mcpi's real entry file is checked.
- Once buildable, the scan found @modelcontextprotocol/ext-apps (a root
  runtime dependency reachable through cli's noExternal reach-in) had
  been inlined into mcpi's bundle instead of staying external. cli and
  tui already list it as external; added the same entry to mcpi's tsup
  config.

npm run validate and npm run coverage both pass cleanly at the repo
root after these changes.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Previously both fell into registerRpcCommands()'s argument-less default
case (a side effect of the generic loop over SESSION_RPC_METHODS), so
skills/get could parse and run but could never succeed: there was no
way to pass --uri. Add dedicated Commander cases for skills/list and
skills/get (positional/--uri for the latter), both with --verify.

--verify's NDJSON report, stderr summary, and non-zero exit code were
also being silently dropped crossing the daemon socket (RpcResult only
carried lines) and, in text mode, forced through the tools/list
--app-info human formatter (wrong shape). Thread summary/exitCode
through the daemon protocol and dispatch layer, add a dedicated
formatSkillVerifyListHuman, and apply the verify exit code the same
way NO_APP/TOOL_ERROR already are.

Verified end-to-end against a live skills-enabled server: skills/list,
skills/list --verify (exit 7 nonconformant / 0 clean), skills/get
<uri>, skills/get --uri --verify, in both --format text and json.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
sessions/list, connect, and sessions/use all returned SessionInfo with
no era field, so seeing which era a session negotiated required a
separate sessions/show <name> lookup per session - awkward with
several open sessions against different servers, and the exact
visibility dual-era support is supposed to give.

Move protocolEra onto the base SessionInfo type (SessionShowResult
already had it; it is now inherited rather than redeclared) and
populate it from client.getProtocolEra() - already connected, so free
- at all three SessionInfo construction sites in SessionRegistry
(list/use/connect). formatSessionsListHuman renders it inline
(@name (MRU) - server [era]); formatSessionInfoHuman already handled
protocolEra being present without a protocolVersion, so connect/
sessions/use output picks it up with no further changes.

Verified end-to-end: connect, sessions/list (text + json), and
sessions/use against a live server all show [legacy]/Era: legacy.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add the one remaining concrete gap flagged in the era-support review:
tasks/update was absent from SESSION_RPC_METHODS entirely, so a modern
(SEP-2663) task paused on input_required could be observed (tasks/get)
or killed (tasks/cancel) from mcpi but never resumed by hand.

- method-types.ts: add "tasks/update" to SESSION_RPC_METHODS; add
  inputResponsesJson to MethodArgs (mirrors roots/set's rootsJson
  JSON-blob convention).
- run-method.ts: add a tasks/update case validating --task-id and
  parsing --input-responses as a JSON object, then calling
  InspectorClient.updateRequestorTask; echoes {updated, taskId} since
  the ack is empty and status only advances on a later tasks/get poll.
- mcp.ts: dedicated Commander case (positional [taskId] / --task-id,
  --input-responses <json>), consistent with the existing tasks/get,
  tasks/cancel, tasks/result case.
- mcp-coverage.test.ts: cover the missing-input-responses error,
  invalid-JSON error, and the call-through path.

Verified against a live tasks-modern-http.json server: a task-augmented
tools/call paused on input_required, tasks/update with the confirm
input resumed it successfully; missing/invalid --input-responses both
fail with clear errors. Full mcpi suite (120 tests) and coverage gate
pass; cli suite (384 tests) and typecheck pass in both packages.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
run-method.ts's tasks/update case (added in 22d5c9f) wasn't exercised
by the cli package's own test suite — only through mcpi's build, which
doesn't count toward this package's coverage gate. That dropped
run-method.ts branch coverage to 85.81%, below the 90% per-file
threshold, and failed CI's coverage job.

Add updateRequestorTask to the mocked client and cover: the success
path (result shape, args forwarded to updateRequestorTask), missing
taskId, missing --input-responses, invalid JSON, and non-object JSON
(array). run-method.ts is back to 93.24% branch coverage; full cli
suite (384 tests) passes.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…s/update)

Closes the last open item from the mcpi era-support review: add a
'Protocol era support' section to the mcpi README covering the
--era connect flag (legacy/auto/modern, overrides catalog/config,
only way to set it for an ad-hoc target), the [era] annotation now
shown inline in sessions/list, sessions/use, and connect output,
sessions/show's fuller era/version/capabilities/supported-versions
detail, and how to resume a paused modern task with tasks/update.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds duplex daemon-IPC framing so a pending elicitation on the
InspectorClient can pause an in-flight tools/call over the daemon
socket, prompt the user in mcpi, and resume the call with the answer.

- protocol.ts: new ElicitationRequestFrame/ElicitationResponseFrame
- ipc-glue.ts: ElicitationChannel + ConnectionElicitationChannel for
  per-connection pause/resume duplex exchange
- elicitation-bridge.ts (new): wires InspectorClient's
  newPendingElicitation events to the daemon's ElicitationChannel,
  skipping task-input-required origin (out of scope for Phase 1)
- server.ts: threads the channel through handle/handleOutcome/
  dispatch/runRpc, wrapping runMethod() with the bridge
- sessions.ts: advertise elicit: { url: true } capability
- client.ts (callDaemon): duplex line handling for elicitation
  request/response frames, with an onElicitation callback
- elicitation-prompt.ts (new): terminal UI for URL-mode accept/
  cancel; form-mode auto-declines (not yet implemented) and
  non-interactive callers auto-cancel
- dispatch.ts: wires prompting in for interactive text-format
  sessions only

Covers legacy elicitInput({mode:"url"}) and modern non-task MRTR
elicitation. Task-augmented MRTR elicitation and form-mode rendering
are deferred (see work/mcpi-era-support.md).

Manually verified end-to-end against test-servers/configs/
url-elicitation-form.json: accept, cancel, and non-interactive
auto-cancel paths all round-trip correctly through the daemon.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds terminal rendering for form-mode elicitation, building on the
Phase 1 duplex daemon-IPC framing.

- session/form-schema.ts (new): parses a requestedSchema into an
  ordered list of typed fields, per the MRTR elicitation spec's
  restricted primitive-field shape (string/number/integer/boolean,
  single-select enum via enum or titled oneOf, multi-select enum via
  array + items.enum/items.anyOf). Returns null for anything outside
  that shape so callers can fall back to a clear decline.
- session/form-prompt.ts (new): prompts once per field (text/numeric/
  y-n/numbered single-select/numbered comma-separated multi-select),
  pre-fills defaults, validates required/length/range/min-max-items
  with a retry loop, then a review step (submit / re-edit a field by
  name / cancel) before returning the answer.
- session/elicitation-prompt.ts: form-mode branch now renders via
  form-schema/form-prompt instead of always declining; still declines
  when non-interactive or when the schema doesn't parse.
- daemon/sessions.ts: advertise elicit: { url: true, form: true }.

Also fixes prettier formatting on the Phase 1 files that hadn't been
run through npm run format:check before pushing (client.ts,
ipc-glue.ts, and a few test files) - this is what broke the build
check on ac7e4bf.

Manually verified end-to-end against test-servers/configs/
modern-mrtr-http.json (boolean field, mrtr_confirm) and
mrtr-showcase-http.json (string field, multi-round MRTR,
mrtr_two_step): accept, cancel-at-review, edit-a-field-at-review, and
non-interactive auto-decline all round-trip correctly through the
daemon.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds a per-connection override for the elicitation capability mcpi
advertises to a server, mirroring the existing --era mechanism:

- InspectorServerSettings.elicitCapability ("off"|"url"|"form"|"both",
  default "both") persists on disk as elicitCapability, omitted when it
  equals the default, and round-trips through serverList.ts the same
  way protocolEra does.
- mcpi connect gains --elicit <mode>, validated the same way as --era,
  with a withElicitOverride() helper mirroring withEraOverride() (incl.
  synthesizing bare-defaults settings for ad-hoc targets).
- createSessionClient() now derives the InspectorClient elicit option
  from serverSettings.elicitCapability via elicitCapabilityToClientOption()
  instead of the old Phase-1 hardcoded { url: true, form: true }.

This lets a caller that cannot handle an interactive elicitation prompt
(a script, an agent) opt out entirely so the server sees no elicitation
capability and can fall back to its own alternative, instead of every
elicitation request being auto-declined.

Also updates clients/mcpi/README.md with an "Elicitation support"
section (previously undocumented, despite already-shipped URL/form
prompt rendering) and the --elicit flag, and refreshes the stale
"Sampling / elicitation CLI: Still TUI/web" to-do row in
specification/v2_cli_v2.md.

Manually verified end-to-end against the modern-mrtr-http test server:
--elicit off makes the server itself reject the mid-round input request
("capabilities do not declare the required capability"); --elicit both
(default) succeeds and reaches the interactive/auto-decline prompt path
as before.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add mcpi's build output to the root package's `bin`/`files` so that
`npm install -g @modelcontextprotocol/inspector` also installs the
`mcpi` binary alongside `mcp-inspector`. mcpi's tsup build already
bundles all monorepo-internal code, and root already declares every
third-party dependency mcpi needs, so no new dependencies or CI risk
are introduced.

- package.json: add "mcpi" bin entry and "clients/mcpi/build" to files
- README.md / AGENTS.md: update project-layout notes to reflect that
  mcpi is now bundled into the published package
- clients/mcpi/README.md: add an "Install" section documenting
  `npm install -g @modelcontextprotocol/inspector` as the real-world
  install path, and rename the old install section to "Build / run
  from this repo (development)" to distinguish it from end-user install

Verified with npm pack + an isolated global install from the packed
tarball: both mcp-inspector and mcpi bins resolve and run correctly
with no missing dependencies.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add a repo-root skills/mcpi/SKILL.md — a concise, usage-only guide to
mcpi for coding agents/LLMs, installable via `npx skills add
modelcontextprotocol/inspector --skill mcpi`. Ship it in the published
npm package (added to root package.json's `files`) so it's also
reachable from a plain `npm install -g @modelcontextprotocol/inspector`
with no git/GitHub access needed.

- clients/mcpi/src/session/mcp.ts: add `mcpi agent-help` (prints the
  SKILL.md content) and `mcpi agent-help --path` (prints its resolved
  file path); add a short "Agent skill for mcpi" pointer between the
  root command's description and its Options list.
- package.json: add "skills/mcpi" to files.
- skills/mcpi/SKILL.md: new — connect/run/target-session/disconnect
  usage, --format, catalog vs. ad-hoc targets, auth, elicitation, and
  session persistence across invocations. Deliberately excludes
  install instructions and daemon implementation details — assumes
  mcpi is already installed and documents only what's needed to use it.

Verified: mcpi `npm run check` + `npm test` (184/184), and an isolated
global install from a packed tarball resolving `mcpi agent-help` /
`mcpi agent-help --path` correctly with no monorepo context present.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
mcpi runs as a front-end for whatever invokes it — a human at a terminal,
or an agent without a real TTY on stdin/stderr — but it inherited the
one-shot CLI's TTY refuse-gate, which exists to protect unattended CI from
hanging up to 15 minutes. That assumption doesn't hold for mcpi: an agent
without a TTY is still expected to relay the printed URL to an attended
human, not run unattended. mcpi now always admits interactive OAuth
(`isTTY: true`), unless `--stored-auth-only` opts out entirely.

Since the printed line can no longer assume a human is reading the
terminal directly, mcpi wires a custom `promptMessage` into
`createCliOAuthNavigation` (new option, additive, default preserved for
the one-shot CLI): real TTY still gets "Please navigate to: <url>"; no
TTY gets "The user needs to navigate to this link to authenticate: <url>"
so an agent knows it must relay the link rather than treat it as its own.

Live-testing this surfaced a real, pre-existing bug: mcpi never wired a
shared `autoOpenControl` between the navigation and the OAuth call, so the
printed line's `armed` gate was always false and nothing was ever printed,
in either mode. Fixed by wiring the same disarmed-until-armed control the
one-shot CLI uses.

Also live-tested: interactive OAuth taking longer than the daemon's 60s
idle timeout (very plausible for a real human login — SSO, MFA) left the
pre-auth daemon having self-exited by the time mcpi retried the connect,
producing a confusing `daemon_unreachable` error right after a successful
login. Fixed by re-calling `ensureDaemon()` before the retry instead of
reusing the pre-OAuth socket path; `ensureDaemon()` is already idempotent
(pings first, only respawns if unreachable), so this is a no-op when the
daemon is still alive.

Finally, add a SIGINT/SIGTERM handler around the OAuth callback wait
(`core/auth/node/runner-interactive-oauth.ts`, shared with the one-shot
CLI and mirrors the existing streaming-command pattern in mcpi's
dispatch.ts): Ctrl-C or a kill now rejects cleanly with "OAuth
authorization cancelled (SIGINT/SIGTERM)." instead of an abrupt process
death, classified as the existing `auth_required` exit code.

All changes verified live end-to-end against a local OAuth test server
(URL print, consent, loopback callback, token exchange, tools/call), not
just via unit tests.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CI's coverage gate (per-file, >=90% on all four dimensions) was failing
for src/session/mcp.ts (88% functions, 88.1% branches). This predates
the OAuth work in the previous commit -- it was already failing on the
prior "Add mcpi agent-help command" commit too, since that added the
agent-help command (and its resolveAgentSkillPath helper) with zero
test coverage. It went unnoticed locally because validate:mcpi runs
vitest run without --coverage, and only coverage:mcpi (which CI's
coverage job runs separately) enforces the per-file threshold.

Add the missing coverage:
- New __tests__/agent-help.test.ts: exercises "mcpi agent-help" (prints
  SKILL.md content) and "mcpi agent-help --path" (prints resolved path).
- __tests__/mcp-coverage.test.ts: exercise the previously-unregistered
  servers/show, skills/list (with/without --verify), and skills/get
  (positional and --uri forms) RPC command actions over a live
  connected session.

Verified locally: "cd clients/mcpi && npx vitest run --coverage" now
exits 0 with mcp.ts at 96.66/92.43/100/96.92 (stmts/branch/funcs/lines),
all above the 90% gate. Also re-checked coverage:cli and coverage:web
(cover the other files touched by the previous commit) -- both clean.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Elicitation (both URL mode and form mode) was gated on a real TTY the
same way OAuth used to be: `interactive` required
`stdin.isTTY && stdout.isTTY`, so a piped/non-TTY stdin (an agent
relaying prompts to a human, or answering on their behalf) always got
an immediate decline/cancel instead of a chance to answer.

That gate wasn't technically necessary. promptForm()/promptField() and
the URL-mode confirm all just use readline's question() over
process.stdin/stderr -- plain line-based I/O, no raw-mode or TTY
features involved. An agent piping answers back on stdin after reading
the prompts on stderr works exactly like a human typing at a terminal.

Changes:
- dispatch.ts: `interactive` is now `format === "text"` (drop the
  isTTY checks). Only `--format json` still auto-declines/cancels,
  since its stdout is a single machine-readable payload with no room
  to interleave a prompt.
- elicitation-prompt.ts: reworded the two "requires an interactive
  terminal" messages to reflect the real reason (`--format json`), and
  raced the URL-mode confirm's rl.question() against a new
  watchForClose() signal (see below).
- form-prompt.ts: added watchForClose(rl)/ask() -- every rl.question()
  in promptField() and the review step now races against the
  readline interface's "close" event. Previously, if stdin closed
  before an answer arrived (e.g. `mcpi ... </dev/null`, or any
  genuinely non-interactive invocation), rl.question() would hang
  forever waiting for a line that would never come. Now it rejects
  promptly, and the caller's existing catch-all in
  promptElicitation() converts that into a clean decline/cancel.

Verified live end-to-end with a real MCP test server (the
`collect_elicitation` preset, which sends a real form-mode elicitation
over a stdio connection):
- `mcpi tools/call ... </dev/null` (stdin already closed): declines
  immediately, no hang.
- Same call with stdin as a live FIFO, writing an answer after the
  prompt appears (simulating an agent relaying/answering): went
  through the field prompt, the review step, and returned
  `{"action":"accept","content":{"name":"Ada Lovelace"}}`, exactly
  like a human at a terminal would.

Also re-ran `validate:mcpi` (191 tests, format/lint/typecheck clean)
and `coverage:mcpi` (per-file gate, all green) after these changes.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Previously SIGINT/SIGTERM handling only existed for the two
STREAM_METHODS (logging/tail, resources/subscribe). Every other
session RPC -- tools/call, connect, an elicitation wait, etc -- had no
signal handler at all, so Ctrl-C during one of those just killed the
process abruptly instead of cancelling cleanly (the same class of
problem the OAuth SIGINT fix addressed earlier this session, just for
a different code path).

- daemon/client.ts: callDaemon() now accepts a `signal?: AbortSignal`
  option (mirroring streamDaemon's existing one). On abort it fails the
  in-flight call with a clear `'<op>' cancelled.` error
  (`code: "cancelled"`, exit code USAGE) instead of leaving the socket
  to time out or the process to die uncleanly.
- session/dispatch.ts: wires an AbortController + SIGINT/SIGTERM
  handlers around the general `callDaemon("rpc", ...)` call the same
  way STREAM_METHODS already does, listeners removed in `finally`.

Also fixed a stale doc comment: daemon/protocol.ts's
`SessionNameParams.requireExplicit` said the front-end sets it from
`!process.stdout.isTTY`, but dispatch.ts actually keys off `stdin` (so
piping output, e.g. `mcpi tools/list | jq`, still uses MRU when a human
is at the keyboard) -- the comment just hadn't been updated to match.

New tests:
- __tests__/dispatch.test.ts: SIGINT/SIGTERM now aborts the general rpc
  call too (not just streams), and listeners are removed once the call
  settles (no leak across calls).
- __tests__/elicitation-client.test.ts: a real socket that never
  responds is aborted mid-call via signal.abort(), rejecting promptly
  with the cancellation error rather than hanging until timeoutMs.

Verified: validate:mcpi (194 tests, format/lint/typecheck clean) and
coverage:mcpi (per-file gate, all green).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@BobDickinson

Copy link
Copy Markdown
Contributor Author

mcpi work summary — PR #1783 (2026-09-13 → 2026-09-14)

Branch: v2/mcpi-client in /Users/bob/Documents/GitHub/inspector-trees/v2-mcpi-client
Repo: modelcontextprotocol/inspector
PR: #1783 — all pushed; CI (build, coverage) green as of 7fd4af59.

Organized by what actually changed functionally, not commit order.


1. Modern protocol-era support (era + skills primitives)

Gave mcpi first-class awareness of MCP's protocol eras (legacy vs. modern/
task-capable) and filled in missing skills primitives:

  • --era override on connect (f2fc1a2c): force which protocol era an
    ad-hoc session negotiates as, instead of only auto-detecting.
  • sessions/show replaces initialize (9550b32c): the session-info RPC
    now reports era details directly (protocol version, task support, etc.)
    instead of the old bare initialize response.
  • protocolEra surfaced everywhere (380dd3e7): every session listing
    (sessions/list, not just sessions/show) now reports era at a glance.
  • tasks/update (22d5c9f4): implemented to resume paused "modern"
    (task-capable) MCP tasks, with success/error-path tests.
  • skills/list and skills/get (40b4f441): implemented the RPCs
    (previously stubbed/missing), supporting positional and --uri argument
    forms, plus a --verify flag on skills/list.
  • Docs (2a61f427): documented --era, sessions/show, and
    tasks/update end-to-end.

2. Elicitation features (legacy URL-mode and modern/MRTR form-mode)

Built out MCP's elicitation flow, covering both eras' mechanisms:

  • URL-mode (legacy elicitation) (ac7e4bf1): when a server elicits via a
    URL, mcpi prompts to confirm/open it and waits for completion.
  • Form-mode (modern/MRTR structured elicitation) (258d789f): when a
    server elicits structured form data (JSON-schema-driven, per the newer
    request-response/MRTR-style pattern), mcpi walks the user through each
    field interactively with a review step before submitting.
  • --elicit capability override (98a41510): lets a caller declare
    elicitation support explicitly, for ad-hoc/non-standard clients.

3. Making mcpi agent-friendly

Everything else — reframing and hardening mcpi so an AI agent driving it
non-interactively gets the same guarantees a human at a terminal gets:

  • Packaging (29193232): bundled mcpi into the published
    @modelcontextprotocol/inspector npm package so it actually ships.
  • mcpi agent-help + skills/mcpi/SKILL.md (9ecd2647): a discoverable,
    self-contained reference for agents on how to drive mcpi non-interactively.
  • OAuth without a TTY (0096e2c7): OAuth's URL-prompt-and-wait flow no
    longer requires an interactive terminal; message reframed for an
    agent-attended flow ("The user needs to navigate to this link to
    authenticate: <url>"). Added clean SIGINT/SIGTERM cancellation so a user
    (or agent) can break out of the ~15-minute OAuth wait if they decide not to
    auth or auth fails, instead of it being a hard, uninterruptible block. Also
    addressed the daemon idle-timeout interacting with long OAuth waits.
    Live-tested with a real, non-TTY OAuth flow.
  • Non-TTY elicitation (7cf45384): removed the TTY gate on elicitation
    entirely — both URL-mode and form-mode now work non-interactively, since
    the underlying readline-based prompting was never actually TTY-dependent,
    just gated by policy. Closed the one real risk this exposed (stdin EOF/close
    could hang readline.question() forever) by racing every prompt against a
    "stdin closed" signal. Live end-to-end tested against a real MCP test
    server, including a piped-EOF instant-decline case and a live-FIFO
    simulated-agent-relayed-answer case.
  • Final non-TTY audit + SIGINT cleanup (7fd4af59): audited all
    remaining isTTY gates; confirmed auth/clear --all and
    requireExplicitSession()'s explicit-session requirement are intentional
    (see MRU note below), fixed a stale doc comment, and extended clean
    SIGINT/SIGTERM cancellation from the two streaming RPCs to the general
    rpc path so Ctrl-C during any blocking call (e.g. tools/call, an
    elicitation wait) cancels cleanly instead of killing the process.

Key design note (MRU): the daemon is a single shared process, so MRU
("most recently used" session) state is global, not per-terminal.
requireExplicitSession() gates on stdin, not stdout, so a human piping
output (mcpi tools/list | jq) still gets MRU convenience; a truly
non-interactive caller (agent/script/CI) must pass --session/@name
explicitly, since there's no live human to catch a wrong guess.
MCP_ALLOW_DEFAULT_SESSION=1 opts back into MRU for scripts that want it.


Non-functional maintenance (excluded from the above as "not changes")

These kept the branch buildable/green but didn't change behavior:

  • e79dea7f, fd64afff — restored build:dev tooling/build config after a
    v2/main merge broke it.
  • 54d00b91 — brought mcpi's validate scripts into parity with the rest of
    the repo's guards.
  • aabc19fa, 12353bea — closed CI coverage/build gaps (including one
    caused by the agent-help commit itself shipping without tests) — pure
    test-coverage backfill, no functional change.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

v2 Issues and PRs for v2

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Inspector mcpi client

2 participants