`ink-form` and `ink-scroll-view` were externalized by the TUI bundler and
both declare a loose `react` peer (">=18"). Installed under a project that
has its own React 18, npm hoists them to that project's root — where the
peer is satisfied — while the Inspector's React 19 nests underneath. The
bundle then renders through React 19 while those two call hooks on React
18, whose dispatcher is null, so opening a tool test form (or any scroll
view) dies with "Cannot read properties of null (reading 'useState')".
Inline them instead: their `import "react"` is emitted into the bundle and
so resolves from the build directory exactly like the bundle's own, with
npm's placement no longer part of the decision. This also pins their
transitive deps to what this repo resolved — notably `ink-select-input@6`
via `overrides`, which npm ignores for a package installed as a dependency,
so consumers were getting the React-18-era v5.
`ink` stays external: it can't be bundled (its CJS `signal-exit@3` fails
ESM interop with `Dynamic require of "assert"`) and doesn't need to be —
its ">=19" peer keeps npm from hoisting it next to an unusable React.
Both packages are dropped from the root `dependencies`, since the tarball
now ships their code rather than having consumers install them.
`clients/tui/__tests__/tsupConfig.test.ts` is the durable guard: the
failure is invisible in this repo and in every smoke (a dev install has
one React) and only appears once the package is installed under another
project, so the invariant is asserted against the config instead — every
dependency declaring a `react` peer must be bundled unless exempted.
Closes #1952
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WLYmVPVLdeYpE6aMTUsCks
140 lines
10 KiB
Markdown
140 lines
10 KiB
Markdown
# MCP Inspector TUI Client
|
|
|
|
The Terminal User Interface (TUI) client brings the interactive exploration capabilities of the Web Client directly to your terminal. It is built using [Ink](https://github.com/vadimdemedes/ink) to provide a rich, React-like component experience in a command-line environment.
|
|
|
|

|
|
|
|
## Running the TUI
|
|
|
|
You can run the TUI client via `npx`:
|
|
|
|
```bash
|
|
npx @modelcontextprotocol/inspector --tui node build/index.js
|
|
```
|
|
|
|
### With Configuration Files
|
|
|
|
The TUI can load all servers from an MCP catalog/config file. With no source
|
|
flag it uses the default writable catalog `~/.mcp-inspector/mcp.json` (seeded
|
|
empty if missing):
|
|
|
|
```bash
|
|
npx @modelcontextprotocol/inspector --tui --catalog mcp.json # writable catalog (seeded empty if missing)
|
|
npx @modelcontextprotocol/inspector --tui --config mcp.json # read-only session (errors if absent)
|
|
```
|
|
|
|
(It does not use `--server`; all servers in the file are available in the TUI.)
|
|
|
|
## Options
|
|
|
|
### MCP server (which server(s) to connect to)
|
|
|
|
Options that specify the MCP server(s) (catalog/config file, ad-hoc command/URL, env vars, headers) are shared by the Web, CLI, and TUI and are documented in [MCP server configuration](../../docs/mcp-server-configuration.md): `--catalog` (writable catalog, seeded **empty** if missing; default `~/.mcp-inspector/mcp.json` or `MCP_CATALOG_PATH`), `--config` (read-only session, errors if absent), `-e`, `--cwd`, `--header`, `--transport`, `--server-url`, and the positional `[target...]`. `--catalog` and `--config` are mutually exclusive, and neither combines with an ad-hoc target.
|
|
|
|
### TUI-specific (OAuth for HTTP servers)
|
|
|
|
The TUI supports OAuth for **SSE** and **Streamable HTTP** servers. Per-server OAuth fields in `mcp.json` (static client id/secret, scopes, enterprise-managed flag) are applied automatically when loaded from `--catalog` or `--config`. Install-wide settings (CIMD, enterprise IdP) come from **`~/.mcp-inspector/storage/client.json`** — the same file the web **Client Settings** dialog writes. You can point at a different file with `--client-config` or `MCP_CLIENT_CONFIG_PATH`.
|
|
|
|
#### OAuth callback URL
|
|
|
|
The TUI starts a small loopback HTTP server to receive the authorization redirect after you sign in in the browser. Defaults:
|
|
|
|
| Surface | Default callback |
|
|
| ------- | ------------------------------------------------------------------------------------------------- |
|
|
| **Web** | `http://localhost:6274/oauth/callback` (main app server) |
|
|
| **TUI** | `http://127.0.0.1:6276/oauth/callback` (dedicated runner port; avoids colliding with web on 6274) |
|
|
|
|
**Why a fixed default port?** Enterprise-managed auth (EMA), CIMD, and many static OAuth apps require **pre-registered redirect URIs**. A predictable default (`http://127.0.0.1:6276/oauth/callback`) lets you register once on the IdP and reuse it across TUI sessions. Dynamic registration (DCR) can use ephemeral ports instead — see below.
|
|
|
|
**Trade-off:** only **one TUI OAuth flow at a time** can listen on the default port. A second instance starting OAuth while another is in progress may fail with `EADDRINUSE`. Override the listener with `--callback-url` or `MCP_OAUTH_CALLBACK_URL` (e.g. a different fixed port per instance, or `http://127.0.0.1:0/oauth/callback` when the authorization server accepts dynamically registered redirect URIs).
|
|
|
|
OAuth redirect URIs must match **exactly** what you register on the authorization server — `localhost` and `127.0.0.1` are different URIs. Register the TUI default on your OAuth app / IdP when using pre-registered (static), CIMD, or enterprise-managed clients. Override the listener with `--callback-url` or `MCP_OAUTH_CALLBACK_URL`; use `http://127.0.0.1:0/oauth/callback` for an OS-assigned ephemeral port when the authorization server registers redirect URIs dynamically (DCR).
|
|
|
|
#### Flags
|
|
|
|
| Option | Env | Description |
|
|
| ----------------------------- | ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
| `--client-config <path>` | `MCP_CLIENT_CONFIG_PATH` | Install-level client config (default: `~/.mcp-inspector/storage/client.json`). |
|
|
| `--client-id <id>` | — | OAuth client ID (static client); overrides `client.json`. |
|
|
| `--client-secret <secret>` | — | OAuth client secret (confidential clients); overrides `client.json`. |
|
|
| `--client-metadata-url <url>` | — | Client ID Metadata Document URL (CIMD); overrides `client.json`. |
|
|
| `--callback-url <url>` | `MCP_OAUTH_CALLBACK_URL` | OAuth redirect/callback listener (default: `http://127.0.0.1:6276/oauth/callback`). Must bind a **loopback** host (`localhost` / `127.0.0.0/8` / `[::1]`); a non-loopback host hard-errors, since the listener receives the authorization code over plaintext `http`. |
|
|
|
|
#### Authenticating in the TUI
|
|
|
|
1. Select an HTTP/SSE server and press **C** to connect.
|
|
2. If authorization is required, the TUI starts OAuth automatically (browser opens for sign-in).
|
|
3. After the callback completes, connect finishes without a second **C**.
|
|
4. Use the **Auth** tab to inspect OAuth state (same fields as web Connection Info) or **Clear OAuth state** (disconnects when connected).
|
|
|
|
See also [EMA / enterprise-managed auth](../../specification/v2_auth_ema.md) and [OAuth smoke testing](../../specification/v2_auth_smoke_testing.md) for staging servers and verification steps.
|
|
|
|
## Features
|
|
|
|
The TUI provides terminal-native tabs and panes for interacting with your MCP server:
|
|
|
|
- **Resources**: Browse and read resources exposed by the server.
|
|
- **Prompts**: List and test prompts.
|
|
- **Tools**: View available tools and execute them with form-like inputs.
|
|
- **Protocol**: View JSON-RPC request/response/notification history (matches the web Protocol monitor).
|
|
- **Network**: View HTTP fetch traffic for SSE / Streamable HTTP servers (matches the web Network monitor).
|
|
- **Console**: View stdio stderr from the connected server process (matches the web Console monitor).
|
|
|
|
## Navigation
|
|
|
|
- Use the **Arrow Keys** (Left/Right) or **Tab** to switch between the main tabs (Resources, Tools, Prompts, etc.).
|
|
- Use the **Arrow Keys** (Up/Down) to scroll through lists of items.
|
|
- Press **Enter** to select an item, execute a tool, or fetch a resource.
|
|
- Press **Escape** or `Ctrl+C` to exit the application.
|
|
|
|
## Development
|
|
|
|
Like the other clients, the TUI self-validates from its own folder:
|
|
|
|
```bash
|
|
npm run validate # format:check && lint && build && test:coverage
|
|
npm test # run all tests
|
|
npm run test:coverage # run tests under the per-file coverage gate
|
|
```
|
|
|
|
The repo-root `validate:tui` just delegates here. `eslint.config.js` registers
|
|
`react-hooks` for the classic rules only (rules-of-hooks + exhaustive-deps); the
|
|
stricter react-hooks@7 rules are not enforced on the interim component surface
|
|
(#1501).
|
|
|
|
Tests live in `__tests__/`. The coverage gate currently covers the TUI's
|
|
non-React logic (server resolution, logger, tab metadata, and the `utils/`
|
|
form/URL helpers); the Ink components and `App.tsx` are an interim exclusion in
|
|
`vitest.config.ts` pending a renderer-based follow-up.
|
|
|
|
### Bundling: React-rendering dependencies must be inlined (#1952)
|
|
|
|
`tsup.config.ts` splits the TUI's dependencies into bundled (`noExternal`) and
|
|
externalized (`external`). For anything that **renders React components**, that
|
|
choice decides which React instance it gets at runtime, and getting it wrong
|
|
crashes the TUI in a way nothing in this repo can see.
|
|
|
|
An external package's `import "react"` resolves from wherever npm placed **that
|
|
package** — and npm places it next to a React satisfying its own peer range.
|
|
`ink-form` and `ink-scroll-view` both accept `react: ">=18"`, so a project that
|
|
installs the Inspector alongside its own React 18 satisfies them: they hoist to
|
|
that project's root while the Inspector's React 19 nests underneath. The bundle
|
|
then renders through React 19 while those two call hooks on React 18, whose
|
|
dispatcher is null — so opening a tool test form (or any scroll view) dies with
|
|
`TypeError: Cannot read properties of null (reading 'useState')`.
|
|
|
|
Bundling them removes npm from the decision: their `import "react"` is emitted
|
|
into `build/index.js`, so it resolves from the build directory exactly like the
|
|
bundle's own. It also pins their transitive deps to what *this* install
|
|
resolved — notably `ink-select-input@6` via the `overrides` entry, since npm
|
|
ignores a dependency's `overrides` and a consumer install would otherwise pull
|
|
the React-18-era v5 that `ink-form` asks for.
|
|
|
|
`ink` itself stays external: it can't be bundled (its CJS `signal-exit@3`
|
|
dependency fails ESM interop with `Dynamic require of "assert" is not
|
|
supported`), and it doesn't need to be — its `react` peer is `">=19"`, which
|
|
keeps npm from hoisting it next to a React the Inspector couldn't also use.
|
|
|
|
`__tests__/tsupConfig.test.ts` enforces this: every dependency declaring a
|
|
`react` peer must be in `noExternal` unless it is listed as external by design.
|
|
Add a React-rendering dependency, and that test tells you to bundle it.
|