feat(cli): session-oriented mcpi CLI with daemon (#1432) - #1727
feat(cli): session-oriented mcpi CLI with daemon (#1432)#1727BobDickinson wants to merge 12 commits into
mcpi CLI with daemon (#1432)#1727Conversation
Land the living design doc under specification/ so we can iterate there; cross-link from the as-built CLI/TUI launcher and catalog specs. Co-authored-by: Cursor <cursoragent@cursor.com>
|
Note: This is a first pass. There are noted open / future issues in the spec. We're trying to land a base PR to build from. @claude review |
|
Claude finished @BobDickinson's task in 3m 49s —— View job Review: session-oriented
|
Arm the session daemon idle timer on start (and after failed connect) so session-less spawns self-reap. JSON.stringify structured --metadata values. Rename the session bin from mcp to mcpi to avoid PATH collisions, and clarify --relogin as ignore-stored-OAuth (no-op for stdio). Co-authored-by: Cursor <cursoragent@cursor.com>
mcp CLI with daemon (#1432)mcpi CLI with daemon (#1432)
|
OK, Cursor updated the PR description with responses to review comments, which isn't ideal. But I think generally we're good now. |
| npx @modelcontextprotocol/inspector --cli # CLI | ||
| npx @modelcontextprotocol/inspector --cli # one-shot CLI | ||
| npx @modelcontextprotocol/inspector --tui # TUI | ||
| npx --package=@modelcontextprotocol/inspector mcpi servers/list # session CLI |
There was a problem hiding this comment.
This seems like a toe-stubber (different command line for one client). Can the launcher not be the entry point and hand off to the mcpi?
|
@cliffhall — following up on the concern about shipping a separate
Why merge this now
|
|
@claude review |
|
Claude finished @BobDickinson's task in 5m 32s —— View job Review:
|
|
Addressed the latest Claude review items on this branch (now committed): Fixed
Left as follow-ups (non-blocking)
|
|
@claude review |
|
Claude finished @BobDickinson's task in 4m 49s —— View job Review: fixes round 3 (
|
| # | Fix | Where | Test |
|---|---|---|---|
| 1 | One-shot allowlist | handlers/method-types.ts:88 + cli.ts:884 |
method-types.test.ts, methods.test.ts |
| 2 | requestInit/eventSourceInit header redaction |
handlers/servers-list.ts:131 |
servers-list.test.ts |
| 3 | Unconditional env redaction | servers-list.ts:150 |
✓ (empty-key case) |
| 4 | Idle re-arm on any connect failure | sessions.ts:220 |
daemon-coverage.test.ts (bad callback URL) |
| 5 | callDaemon close handler |
client.ts:103 |
daemon-coverage.test.ts (FIN-no-reply) |
| 6 | MRU keys off stdin.isTTY |
dispatch.ts:101 |
dispatch.test.ts |
| 7 | CLI README drift | clients/cli/README.md |
— |
ONE_SHOT_METHODS is exactly the 9 methods the pre-PR cli.ts switch supported (checked against b3b04c69:clients/cli/src/cli.ts:343), so the allowlist restores the old surface rather than narrowing it — and servers/list/servers/show short-circuit above the check, so the error text stays truthful. Packaging walk-back re-verified: root bin is mcp-inspector only, "files" has no clients/mcpi/build, and neither README mentions mcpi.
1. (Low) streamDaemon still treats a pre-response FIN as success
The mirror of fix #5 wasn't applied on the stream side. stream-client.ts:138:
socket.on("close", () => {
if (!settled) succeed();
});The error handler two lines up correctly splits on streaming (:124-136) — soft-end after the ok frame, daemon_unreachable before it. The close handler doesn't. So a daemon that accepts the stream connection and then dies before writing the ok frame makes mcpi logging/tail exit 0 with no output, indistinguishable from a clean end. That's the exact failure callDaemon just stopped doing (there it was a 60s stall; here it's a silent success, which is worse for scripts). The spec To-do row "streamDaemon post-open errors" covers the streaming === true case only — this is the pre-open one. Fix this →
2. (Low) Header redaction misses the array form of HeadersInit
sanitizeInitRecord (servers-list.ts:176) only descends when isPlainObject(init.headers) — and isPlainObject explicitly rejects arrays (:168). RequestInit.headers is Headers | string[][] | Record<string,string>, so a hand-written entry using the pair form:
{ "type": "streamable-http", "url": "…",
"requestInit": { "headers": [["Authorization", "Bearer …"]] } }still prints verbatim under a command whose help says "secrets redacted". Same class as the bug just fixed, one shape over. A few lines in sanitizeInitRecord to also map string[][] closes it.
3. (Nit) metaValueToString is duplicated verbatim across the two clients
clients/cli/src/cli.ts:459 and clients/mcpi/src/session/mcp.ts:809 are byte-identical, comment included. mcpi already reaches into @inspector/cli/handlers/*, so this belongs next to MethodArgs in handlers/method-types.ts — two copies of the metadata-encoding rule is precisely how the one-shot and session paths drift back apart on the thing fix #2-from-last-round was about.
4. (Nit) Method validation runs after the stored-auth work
cli.ts:884 (allowlist) and :855 (Method is required) both sit below the --use-stored-auth / --wait-for-auth block at :790, which can do a token refresh over the network. mcp-inspector --cli --method bogus --use-stored-auth … therefore burns a refresh before emitting a purely local usage error. Pre-existing ordering, cheap to hoist both checks above :790.
5. (Low) The daemon has no last-resort crash handler
daemon/run.ts installs SIGINT/SIGTERM but no unhandledRejection / uncaughtException. The accept path is a voided async IIFE (ipc-glue.ts:34); handleOutcome is total (catch-all at server.ts:135), so the realistic trigger is narrow — encodeResponse throwing on a non-serializable result — but if it ever fires, Node kills the daemon and daemon.sock / daemon.lock are orphaned (removeLockAndSocket only runs in stop()). Recovery does work on the next spawn via removeStaleDaemonSocket, but in between, mcpi daemon status reports a dead pid. Compounding it: ensure.ts:140 spawns with stdio: "ignore", so run.ts:27's process.stderr.write goes to /dev/null — a daemon that fails to start is completely silent, and the parent just sees the generic 10s daemon_start_timeout. A crash handler that calls stop(), and eventually a daemon log file, would make this debuggable.
6. (Doc) specification/v2_cli_v2.md testing section is stale after the extraction
Line 154 says "Per-file ≥90 coverage on measured clients/cli/src" and line 155 lists the exclusions as one set — but mcp-bin.ts / daemon/run.ts / ipc-glue.ts / stream-client.ts now live under clients/mcpi/src and are gated by clients/mcpi/vitest.config.ts, while index.ts is the cli one. Worth splitting into the two clients. While there: the one-shot --method table at :72-75 doesn't state that anything outside it is now a usage error before connect — that's the user-visible half of fix #1.
Notes, not asks:
- Reconnect (
mcpi connect <name>over a live session) leaves an idle timer armed: the innerdisconnectarms it atsessions.ts:245when the registry empties, and the successful re-connect doesn't clear it. It's harmless only because the timer callback re-checkssessions.size === 0at:265— please keep that guard. --relogin's documented conflicts (--stored-auth-only/--use-stored-auth/--wait-for-auth) are actually enforced atcli.ts:727-734. ✓
Nothing here blocks the merge — #1 is the one I'd take, since it's a two-line symmetry fix to the same bug class you just closed on callDaemon, and a silent exit 0 is the hardest failure mode to notice in a script. #2 and #6 are quick follow-ups.
- Gather context (diff vs
origin/v2/main, latest commit) - Verify the 7 claimed fixes + their tests
- Fresh pass over daemon IPC / packaging / docs
- Post consolidated review
· branchv2/cli-v2-sessions
|
Addressed the follow-up Claude review items (commit Fixed
Still follow-up (non-blocking)
|
Resolve conflicts keeping the mcpi client extraction while adopting validate:core, cli/tui typecheck, and related AGENTS/package.json updates from main. Fix servers/list config narrowing for the new CLI typecheck gate. Co-authored-by: Cursor <cursoragent@cursor.com>
Narrow on sse/streamable-http first so optional stdio `type` does not block access to `url` under the new cli typecheck gate from v2/main. Co-authored-by: Cursor <cursoragent@cursor.com>
|
@cliffhall - I think this is mergeable now - it gets us some improved one-shot CLI functionality and gets the mcpi code into v2/main, but out of the package/bundle and hidden from end users (that don't build from source), per: |
|
Splitting this PR to address release concerns about landing
Closing this PR in favor of those two. The tip of |
Closes #1432
Summary
Implements the session-oriented Inspector CLI from #1432, as documented in
specification/v2_cli_v2.md:mcpientrypoint (bin.mcpi→clients/cli/build/mcp-bin.js) with an implicit local session daemon (IPC). Connect once, run many MCP subcommands against a named session, then disconnect — ssh-agent-style private binding viaeval "$(mcpi private)".mcp-inspector --clistill connects → one--method→ disconnect, never starts the daemon. Both paths sharehandlers/run-method.ts+InspectorClient.servers/list|show), session lifecycle (connect/disconnect/sessions/*), auth store (auth/list,auth/clear), daemon control, and MCP methods including streams (logging/tail,resources/subscribe), tasks, and roots.--format json, no{ result }envelope); TTY styling / OSC 8; connect-time OAuth with--relogin/--stored-auth-only.Known gaps stay in the spec To-do (mid-session auth over IPC, Windows named pipes, daemon lock hardening, etc.) — not claimed as done here.
Review follow-ups (Claude on this PR)
Addressed in
5fbd3b9e:ensureDaemonfrom e.g.mcpi tools/listwith no sessions still self-reaps (~60s). Covered by a session-less start test.stringifyMetamangling (Low) — Non-string metadata values (objects/arrays fromkey={"a":1}) are nowJSON.stringify'd instead ofString(v)→"[object Object]", in both session and one-shot paths.--reloginon stdio (Low/UX) — Documented, not rejected: the flag means “ignore stored OAuth for this connect; interactive login only if auth is required.” For stdio / no URL-keyed store entry it is a deliberate no-op (nothing to clear). Help text, README, andv2_cli_v2.mdupdated accordingly.mcp→mcpi(MCP Inspector) in root +clients/clipackage.json, docs, examples, and Commander.name(), to avoid claiming a genericPATHname.Deferred (already in spec To-do / out of scope for this base PR):
ensureDaemonunlink on failed ping vs auth mismatch — rare private-token edge case; leave for later hardening.ipc-glue.ts/stream-client.ts— already tracked in the spec To-do.Test plan
npm run validate(web / cli / tui / launcher)clients/cliunit suite + per-file ≥90 coverage gate (test:coverage), with the exclusions listed in the specmcpi connect <server> --config …→tools/list→tools/call …→disconnect→daemon stopmcpi connect … --relogin,auth/list,auth/clear <url>,auth/clear --all --yeseval "$(mcpi private)"isolates the daemon from the default socketmcp-inspector --cli …still works for the same catalog entrymcpirename