Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
25 changes: 16 additions & 9 deletions AGENTS.md

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ All three run through one global `mcp-inspector` binary:

```bash
npx @modelcontextprotocol/inspector # web UI (default)
npx @modelcontextprotocol/inspector --cli # CLI
npx @modelcontextprotocol/inspector --cli # one-shot CLI
npx @modelcontextprotocol/inspector --tui # TUI
```

Expand All @@ -24,7 +24,7 @@ v2 is **not** an npm workspace. Each client under `clients/*` keeps its own `pac
inspector/
├── clients/
│ ├── web/ # Web client (Vite + React + Mantine). src/ = browser app; server/ = Node dev/prod backend
│ ├── cli/ # CLI client (tsup bundle, @inspector/core alias)
│ ├── cli/ # CLI client (tsup bundle) — one-shot `mcp-inspector --cli`
│ ├── 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 Down
30 changes: 0 additions & 30 deletions clients/launcher/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

78 changes: 78 additions & 0 deletions clients/mcpi/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# MCP Inspector session CLI (`mcpi`)

**Experimental** separate client — not part of the published `@modelcontextprotocol/inspector` package. Connect once, then run many MCP commands against a named session via an implicit local daemon (ssh-agent style).

> **Layout note:** Source lives in `clients/mcpi/`. At build time it bundles some modules from `clients/cli/src` (`handlers/`, `error-handler`, OAuth helpers) via the `@inspector/cli` alias. That reach-in is intentional and temporary — not a published library API — until a cleaner shared package exists.

## Install / run (from this repo)

Build, then put `mcpi` on your PATH with `npm link` (points at this package’s `build/mcp-bin.js`):

```bash
# from the repo root — install deps once if needed
npm install

cd clients/mcpi
npm run build
npm link

mcpi --help
```

Rebuild after pulling source changes (`npm run build` in `clients/mcpi`). You usually do **not** need to re-link unless the package `bin` entry changes.

Without linking, run the built file directly:

```bash
node clients/mcpi/build/mcp-bin.js --help
```

Remove the link when you’re done:

```bash
npm unlink -g @modelcontextprotocol/mcpi
```

## Usage

```bash
mcpi servers/list --config path/to/mcp.json
mcpi servers/show test-stdio --config path/to/mcp.json
mcpi connect test-stdio --config path/to/mcp.json
mcpi connect my-http --config path/to/mcp.json --relogin # ignore stored OAuth; login only if auth required
mcpi auth/list
mcpi auth/clear https://example.com/mcp
mcpi auth/clear --all --yes
mcpi tools/list
mcpi tools/call echo message:=hi
mcpi tools/call echo '{"message":"hi"}'
mcpi @test-stdio resources/list
mcpi logging/tail # long-lived; Ctrl-C to stop
mcpi sessions/list
mcpi disconnect --session test-stdio
mcpi daemon status
mcpi daemon stop

# Optional: private daemon for this shell only
eval "$(mcpi private)"
mcpi connect test-stdio --config path/to/mcp.json
mcpi tools/list
```

**Globals (before subcommand):** `--format text|json`, `--plain`, `--session <name>`, `--catalog` / `--config`, `--stored-auth-only`.

**Output:** `--format text` (default) is human-readable (TTY ANSI unless `--plain` / `NO_COLOR`). `--format json` is pretty-printed payload with **no** `{ result }` envelope.

**Auth:** shared `oauth.json` with other Inspector clients. Connect-time OAuth only on this CLI; mid-session step-up remains on one-shot `mcp-inspector --cli`. `--relogin` clears any URL-keyed store entry before connect (no-op for stdio).

See [`specification/v2_cli_v2.md`](../../specification/v2_cli_v2.md) for the as-built design and to-do list.

## Relation to one-shot CLI

| | One-shot | Session (`mcpi`) |
| --- | --- | --- |
| Entrypoint | `mcp-inspector --cli` | `mcpi` |
| Package (dev) | `clients/cli` | `clients/mcpi` |
| Lifecycle | Connect → one `--method` → disconnect | Connect once → many subcommands |

One-shot docs: [`clients/cli/README.md`](../cli/README.md).
66 changes: 66 additions & 0 deletions clients/mcpi/__tests__/authorize.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { describe, it, expect, vi, afterEach } from "vitest";
import type { MCPServerConfig } from "@inspector/core/mcp/types.js";

const connectSpy = vi.fn();
const disconnectSpy = vi.fn().mockResolvedValue(undefined);

vi.mock("@inspector/cli/cliOAuth.js", () => ({
connectInspectorWithOAuth: (...args: unknown[]) => connectSpy(...args),
}));

vi.mock("@inspector/core/mcp/index.js", () => ({
InspectorClient: class {
connect = vi.fn();
disconnect = disconnectSpy;
},
}));

vi.mock("@inspector/core/client/runner.js", async (importOriginal) => {
const actual =
await importOriginal<typeof import("@inspector/core/client/runner.js")>();
return {
...actual,
loadRunnerClientConfig: vi.fn().mockResolvedValue({}),
buildRunnerClientAuthOptions: vi.fn().mockReturnValue({}),
};
});

describe("authorizeInFrontend", () => {
afterEach(() => {
connectSpy.mockReset();
disconnectSpy.mockClear();
});

it("no-ops for non-OAuth-capable (stdio) configs", async () => {
const { authorizeInFrontend } = await import("../src/session/authorize.js");
await authorizeInFrontend(
{ type: "stdio", command: "x" } as MCPServerConfig,
undefined,
);
expect(connectSpy).not.toHaveBeenCalled();
});

it("runs connectInspectorWithOAuth for HTTP configs", async () => {
connectSpy.mockResolvedValue(undefined);
const { authorizeInFrontend } = await import("../src/session/authorize.js");
await authorizeInFrontend(
{ type: "streamable-http", url: "https://example.com/mcp" },
{ protocolEra: "2025-11-25" } as never,
{ storedAuthOnly: true },
);
expect(connectSpy).toHaveBeenCalled();
expect(disconnectSpy).toHaveBeenCalled();
});

it("swallows disconnect failures in finally", async () => {
connectSpy.mockResolvedValue(undefined);
disconnectSpy.mockRejectedValueOnce(new Error("bye"));
const { authorizeInFrontend } = await import("../src/session/authorize.js");
await expect(
authorizeInFrontend(
{ type: "streamable-http", url: "https://example.com/mcp" },
undefined,
),
).resolves.toBeUndefined();
});
});
Loading
Loading