Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
2bb98b0
feat(cli): catalog listing, auth flags, and OAuth browser UX
BobDickinson Jul 25, 2026
a6bc7e6
feat(mcpi): add experimental session CLI client
BobDickinson Jul 25, 2026
57cb8bd
Merge branch 'v2/main' into v2/mcpi-client
BobDickinson Jul 26, 2026
24f39b3
chore(mcpi): format and pin prettier after v2/main merge
BobDickinson Jul 26, 2026
de0dd23
Merge branch 'v2/main-latest' into v2/mcpi-client
BobDickinson Sep 14, 2026
e79dea7
chore(mcpi): re-add build:dev tooling after v2/main merge
BobDickinson Sep 14, 2026
fd64aff
fix(mcpi): restore build after v2/main merge
BobDickinson Sep 14, 2026
f2fc1a2
feat(mcpi): add --era override to connect for ad-hoc targets
BobDickinson Sep 14, 2026
9550b32
mcpi: replace initialize with sessions/show (adds era details)
BobDickinson Sep 14, 2026
54d00b9
Bring mcpi into parity with repo-wide validate guards
BobDickinson Sep 14, 2026
aabc19f
mcpi: close CI coverage/build gaps found after typecheck fix unblocke…
BobDickinson Sep 14, 2026
40b4f44
fix(mcpi): implement skills/list and skills/get RPC commands
BobDickinson Sep 14, 2026
380dd3e
feat(mcpi): surface protocolEra on every session, not just sessions/show
BobDickinson Sep 14, 2026
22d5c9f
feat(mcpi): implement tasks/update to resume paused modern tasks
BobDickinson Sep 14, 2026
2c3d6ce
test(cli): cover tasks/update success and error paths
BobDickinson Sep 14, 2026
2a61f42
docs(mcpi): document protocol era support (--era, sessions/show, task…
BobDickinson Sep 14, 2026
ac7e4bf
mcpi: support URL-mode elicitation (Phase 1)
BobDickinson Sep 14, 2026
258d789
mcpi: support form-mode elicitation (Phase 3)
BobDickinson Sep 14, 2026
98a4151
mcpi: add --elicit capability override (#1783)
BobDickinson Sep 14, 2026
2919323
Bundle mcpi into the published @modelcontextprotocol/inspector package
BobDickinson Sep 15, 2026
9ecd264
Add mcpi agent-help command and skills/mcpi/SKILL.md
BobDickinson Sep 15, 2026
0096e2c
mcpi: allow interactive OAuth without a TTY, add clean cancellation
BobDickinson Sep 15, 2026
12353be
mcpi: close per-file coverage gaps in mcp.ts (CI fix)
BobDickinson Sep 15, 2026
7cf4538
mcpi: allow non-TTY elicitation prompting (agent-attended)
BobDickinson Sep 15, 2026
7fd4af5
mcpi: clean SIGINT/SIGTERM cancellation for the general rpc path
BobDickinson Sep 15, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
# Inspector V2

This is an application for inspecting MCP servers. It has three incarnations —
Web, TUI, and CLI — over a shared `core/`.
This is an application for inspecting MCP servers. It has four client
surfaces — Web, TUI, one-shot CLI, and the experimental session CLI (`mcpi`) —
over a shared `core/`.

**This file holds the _rules_: the conventions a reviewer cites against a diff.**
It is loaded in full on every turn, so it stays resident and must stay complete
Expand Down Expand Up @@ -40,6 +41,9 @@ inspector/
│ │ ├── server/ Node-only dev/prod backend wiring
│ │ └── static/ sandbox_proxy.html — served for the MCP Apps tab
│ ├── cli/ Scriptable CLI (tsup bundle, @inspector/core alias)
│ ├── mcpi/ Experimental session CLI (`mcpi` bin — connect once, many
│ │ commands; implicit Unix-socket session daemon). Bundled
│ │ into the published package — see clients/mcpi/README.md
│ ├── tui/ Ink + React terminal UI (tsup bundle)
│ └── launcher/ The `mcp-inspector` bin; dispatches to web/cli/tui in-process
├── core/ Shared code, consumed via the `@inspector/core` alias (no package.json)
Expand Down Expand Up @@ -385,12 +389,12 @@ When asked to respond to a code review of a PR:
The _procedure_ — where a given test file goes, which command runs it, how to
diagnose a failing gate — is the `testing` skill. These are the rules.

- **Ensure all code has corresponding tests.** New code must clear **≥ 90 on all four dimensions** — lines, statements, functions, and branches — per file. This gate is enforced by each client's `test:coverage` across `clients/web`, `clients/cli`, `clients/tui` and `clients/launcher`, and **CI enforces it**: a PR that drops any file below 90 on any dimension fails.
- **Ensure all code has corresponding tests.** New code must clear **≥ 90 on all four dimensions** — lines, statements, functions, and branches — per file. This gate is enforced by each client's `test:coverage` across `clients/web`, `clients/cli`, `clients/tui`, `clients/launcher`, and (experimentally) `clients/mcpi`, and **CI enforces it**: a PR that drops any file below 90 on any dimension fails. **mcpi** excludes bootstraps + hard-to-stabilize accept/stream races from the gate (`src/mcp-bin.ts`, `src/daemon/run.ts`, `src/daemon/ipc-glue.ts`, `src/daemon/stream-client.ts` — see `clients/mcpi/vitest.config.ts`); its build-time `@inspector/cli` alias reaches into `clients/cli/src` for shared handlers/error-handler/OAuth helpers (temporary, not a published API).
- **A genuinely-unreachable branch is annotated at the source, never waved through by lowering the gate.** Use a justified `/* v8 ignore … -- <reason> */`. Acceptable reasons: happy-dom-inherent paths (Mantine portal mount points, `useMediaQuery` fallbacks, `typeof window` SSR guards); React StrictMode effect-replay blocks; and provably-dead defensive guards (a `?? fallback` for a value the types guarantee non-null, a `Select.onChange` receiving a value outside the allowed list). Reach for it only when the branch is genuinely impossible to exercise.
- **In unit tests that expect error output, suppress it from the console.**
- **Test placement — side-by-side by default, `src/test/` only for what can't be co-located, and the Node clients are different.**
- **`clients/web`**: `<Name>.test.tsx` **next to the source** — components, hooks, `lib/`, `utils/`. A web-owned test living under `src/test/` instead is a bug. `src/test/` is for the three things that cannot be co-located: tests of the repo-root **`core/`** package (`src/test/core/…`, mirroring the `core/` layout — it lives outside `clients/web/` and has no harness of its own); the **`integration`** project (`src/test/integration/…` — _placement is the manifest_, picked up by a folder glob, with no enumeration to keep in sync); and **shared test infrastructure** (`renderWithMantine.tsx`, `setup.ts`, `fixtures/`).
- **`clients/cli`, `clients/tui`, `clients/launcher`**: **all** tests in a top-level **`__tests__/`**, not beside their source. Their `tsconfig.json` excludes `**/*.test.*`, so a co-located test lands in **no** tsconfig project and fails `npm run verify:typecheck-coverage`.
- **`clients/cli`, `clients/mcpi`, `clients/tui`, `clients/launcher`**: **all** tests in a top-level **`__tests__/`**, not beside their source. Their `tsconfig.json` excludes `**/*.test.*`, so a co-located test lands in **no** tsconfig project and fails `npm run verify:typecheck-coverage`.
- **Root tooling**: a `scripts/*.mjs` helper with pure logic gets a sibling `*.test.mjs`. Keep that exact filename — `node --test` silently _skips_ a file its glob misses and still exits 0.
- **Render Ink components through the TUI's own `render`** (`clients/tui/__tests__/helpers/renderTui.tsx`), never `ink-testing-library`'s directly. It is the same function with every frame ANSI-stripped, which is what keeps an assertion on styled text from depending on the ambient environment: Ink writes styling *inside* the styled run, so `<Text underline>I</Text>nfo` reaches the frame buffer with escapes between `I` and `nfo` and `toContain("Info")` fails. It only bites where chalk emits color — a developer whose shell exports `FORCE_COLOR` — so CI is green on a suite that is broken for them (#2207). A test that genuinely needs the raw bytes reads `stdout.lastFrame()` off the returned instance.
- **Render React components through `renderWithMantine`** (`src/test/renderWithMantine.tsx`); do not hand-roll a bare `MantineProvider`, which skips the project theme and the helper's options and drifts from every other test. Pass the `colorScheme` option to exercise a forced scheme rather than hand-rolling `defaultColorScheme`. Use `renderWithMantineTransitions` **only** when a test must assert mid-flight transition state, and read the long comment on the helper before changing anything about it.
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ inspector/
├── clients/
│ ├── web/ Web client (Vite + React + Mantine). src/ = browser app; server/ = Node backend
│ ├── cli/ CLI client (tsup bundle, @inspector/core alias)
│ ├── mcpi/ Experimental session CLI (`mcpi` bin) — bundled into the
│ │ published package; see clients/mcpi/README.md
│ ├── tui/ TUI client (Ink + React, tsup bundle)
│ └── launcher/ Shared launcher — provides the `mcp-inspector` bin, dispatches to web/cli/tui
├── core/ Shared code consumed via the `@inspector/core` alias (no package.json)
Expand All @@ -63,7 +65,7 @@ inspector/
```

Each client has its own README with client-specific detail:
[web](./clients/web/README.md) · [cli](./clients/cli/README.md) · [tui](./clients/tui/README.md) · [launcher](./clients/launcher/README.md).
[web](./clients/web/README.md) · [cli](./clients/cli/README.md) · [mcpi](./clients/mcpi/README.md) · [tui](./clients/tui/README.md) · [launcher](./clients/launcher/README.md).

## Documentation

Expand Down
35 changes: 35 additions & 0 deletions clients/cli/__tests__/run-method-mocks.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ function mockClient(overrides: Partial<InspectorClient> = {}): InspectorClient {
getRequestorTask: vi.fn().mockResolvedValue({ taskId: "t1" }),
cancelRequestorTask: vi.fn().mockResolvedValue(undefined),
getRequestorTaskResult: vi.fn().mockResolvedValue({ content: [] }),
updateRequestorTask: vi.fn().mockResolvedValue(undefined),
getRoots: vi.fn().mockReturnValue([]),
setRoots: vi.fn().mockResolvedValue(undefined),
setLoggingLevel: vi.fn().mockResolvedValue(undefined),
Expand Down Expand Up @@ -133,6 +134,19 @@ describe("runMethod (mocked client)", () => {
});
expect(result.kind).toBe("result");

const updated = await runMethod(client, {
method: "tasks/update",
taskId: "t1",
inputResponsesJson: '{"confirm":{"approved":true}}',
});
expect(updated.kind).toBe("result");
if (updated.kind === "result") {
expect(updated.result).toMatchObject({ updated: true, taskId: "t1" });
}
expect(client.updateRequestorTask).toHaveBeenCalledWith("t1", {
confirm: { approved: true },
});

const complete = await runMethod(client, {
method: "prompts/complete",
completeRefType: "ref/prompt",
Expand Down Expand Up @@ -191,6 +205,27 @@ describe("runMethod (mocked client)", () => {
/tasks\/result/,
);

await expect(runMethod(client, { method: "tasks/update" })).rejects.toThrow(
/tasks\/update/,
);
await expect(
runMethod(client, { method: "tasks/update", taskId: "t1" }),
).rejects.toThrow(/--input-responses/);
await expect(
runMethod(client, {
method: "tasks/update",
taskId: "t1",
inputResponsesJson: "not-json",
}),
).rejects.toThrow(/--input-responses is invalid/);
await expect(
runMethod(client, {
method: "tasks/update",
taskId: "t1",
inputResponsesJson: "[1,2,3]",
}),
).rejects.toThrow(/--input-responses is invalid/);

await expect(
runMethod(client, {
method: "roots/set",
Expand Down
14 changes: 13 additions & 1 deletion clients/cli/src/cli-oauth-navigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,15 @@ export type CliOAuthNavigationOptions = {
* (`MCP_AUTO_OPEN_ENABLED=true`).
*/
forceAutoOpen?: boolean;
/**
* Build the printed prompt line for a given authorize URL. Receives the
* (possibly OSC-8-linked) display string and whether stderr is a TTY.
* Defaults to the CLI's own "Please navigate to: <url>" framing. Override
* when a different caller needs different wording — e.g. mcpi, addressed to
* whatever is running it (which may be an agent that must relay the link to
* a human) rather than to a human reading the terminal directly.
*/
promptMessage?: (hrefDisplay: string, tty: boolean) => string;
};

/**
Expand Down Expand Up @@ -108,7 +117,10 @@ export function createCliOAuthNavigation(
);
const write =
options.write ?? ((line: string) => process.stderr.write(line));
write(`Please navigate to: ${style.link(href)}\n`);
const promptMessage =
options.promptMessage ??
((hrefDisplay: string) => `Please navigate to: ${hrefDisplay}`);
write(`${promptMessage(style.link(href), tty)}\n`);

const envAllows =
options.autoOpenEnabled !== undefined
Expand Down
17 changes: 15 additions & 2 deletions clients/cli/src/handlers/method-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export type MethodArgs = {
*/
strict?: boolean;
format?: OutputFormat;
/** Task id for tasks/get, tasks/cancel, tasks/result. */
/** Task id for tasks/get, tasks/cancel, tasks/result, tasks/update. */
taskId?: string;
/** When true, tools/call uses callToolStream (task-augmented). */
task?: boolean;
Expand All @@ -48,6 +48,12 @@ export type MethodArgs = {
cursor?: string;
/** roots/set payload (JSON array of {uri, name?}). */
rootsJson?: string;
/**
* tasks/update payload (JSON object keyed by the server's `inputRequests`
* ids). Resumes a modern (SEP-2663) task paused on `input_required` —
* modern-only, symmetric with `roots/set`'s JSON-blob convention.
*/
inputResponsesJson?: string;
/** prompts/complete: argument name / value / ref. */
completeRefType?: "ref/prompt" | "ref/resource";
completeRef?: string;
Expand Down Expand Up @@ -91,9 +97,15 @@ export type MethodOutcome =
* TODO(#1432): several of these (subscribe, tasks, roots, logging/tail, …) are
* not exposed by `mcp-inspector --cli` today; they exist for the experimental
* session CLI (`mcpi`) and other Node runners that share this dispatcher.
*
* Deliberately excludes `"initialize"` — that's still a valid {@link
* ONE_SHOT_METHODS} entry (scripting parity with the literal wire method
* name), but for `mcpi` it read as "send another initialize", which it never
* did (it only replays cached connect-time state). `mcpi sessions/show`
* covers the same data (server info, capabilities, negotiated era) alongside
* daemon session bookkeeping instead.
*/
export const SESSION_RPC_METHODS = [
"initialize",
"tools/list",
"tools/call",
"resources/list",
Expand All @@ -111,6 +123,7 @@ export const SESSION_RPC_METHODS = [
"tasks/get",
"tasks/cancel",
"tasks/result",
"tasks/update",
"roots/list",
"roots/set",
"skills/list",
Expand Down
32 changes: 32 additions & 0 deletions clients/cli/src/handlers/run-method.ts
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,38 @@ export async function runMethod(
result = (await inspectorClient.getRequestorTaskResult(
args.taskId,
)) as McpResponse;
} else if (args.method === "tasks/update") {
if (!args.taskId) {
throw new Error("Task id is required for tasks/update. Use --task-id.");
}
if (!args.inputResponsesJson) {
throw new Error(
"tasks/update requires --input-responses '<json object keyed by inputRequests id>'.",
);
}
let inputResponses: Record<string, unknown>;
try {
const parsed: unknown = JSON.parse(args.inputResponsesJson);
if (
typeof parsed !== "object" ||
parsed === null ||
Array.isArray(parsed)
) {
throw new Error("must be a JSON object");
}
inputResponses = parsed as Record<string, unknown>;
} catch (e) {
throw new Error(
`--input-responses is invalid: ${e instanceof Error ? e.message : String(e)}`,
{ cause: e },
);
}
await inspectorClient.updateRequestorTask(args.taskId, inputResponses);
// The server acks with an empty result and the task's status advances
// only on a subsequent tasks/get poll (updateRequestorTask says so) —
// so echo back what was actually sent rather than imply a fresher
// status is available here.
result = { updated: true, taskId: args.taskId };
} else if (args.method === "skills/list") {
// The store's cursor walk is reused rather than re-implemented — it
// carries the repeated-cursor and page-cap guards, and a second copy of
Expand Down
Loading