Skip to content

Latest commit

 

History

History
213 lines (159 loc) · 21.1 KB

README.md

File metadata and controls

213 lines (159 loc) · 21.1 KB

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 to provide a rich, React-like component experience in a command-line environment.

MCP Inspector TUI Screenshot

Running the TUI

You can run the TUI client via npx:

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):

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: --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, --protocol-era (legacy/auto/modern; sets the era an ad-hoc server negotiates, or overrides a file's protocolEra), --transport, --server-url, and the positional [target...]. --catalog and --config are mutually exclusive, and neither combines with an ad-hoc target.

--skill-catalog-max-skills <n> and --skill-catalog-max-bytes <n> set the per-server skills catalog budget (defaults 256 skills and 67108864 bytes, 64 MiB), the same setting the web client exposes in Server Settings. Each takes a positive integer (anything else is rejected at startup) and overrides the file's skillCatalogMaxSkills / skillCatalogMaxBytes for every server loaded, like --header. The budget bounds a verification run that covers several skills: in the Skills pane that is v, which verifies the whole listing in one run the way the CLI's --verify does, and skills past the budget come back incomplete and unread. Enter verifies one skill, which always reads it in full (#2590).

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

Variables with no flag of their own — the secret store, MCP_INSPECTOR_LOG_DIR, LOG_LEVEL, proxies — are listed in Environment variables.

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). Clearing also revokes the grant at the authorization server when it advertises an RFC 7009 revocation_endpoint — best-effort, with any failure reported in the status line and the local clear finishing regardless. Set oauth.revokeOnClear: false on the server entry to skip it (#2144).

See also EMA / enterprise-managed auth and OAuth smoke testing for staging servers and verification steps.

Features

The TUI provides terminal-native tabs and panes for interacting with your MCP server:

  • Info: The server's configuration and identity, plus the Roots the client advertises to it. With the pane focused, e opens the roots editor (n add, x remove), which applies changes through setRoots and so sends notifications/roots/list_changed. Edits last for the session only; the TUI does not write them back to the catalog (#2432).
  • Resources: Browse and read resources exposed by the server.
  • Subscriptions (u): Shown only when the server advertises resources.subscribe. Enter subscribes to or unsubscribes from the selected resource (legacy resources/subscribe, or the modern subscriptions/listen filter, chosen by era), and the detail pane shows each subscription's last update, the modern listen-stream status, and a live feed of notifications/resources/updated read from the protocol log (#2432).
  • Prompts: List and test prompts.
  • Tools: View available tools and execute them with form-like inputs. A tool whose advertised schema carries a portability problem is flagged in the list — red ! for a construct a shipping MCP client refuses, yellow ? for one handled unevenly — and the detail pane lists each finding under Schema Portability with the path, the problem, and a concrete fix. The verdict comes from core/json/schemaLint.ts, shared with the web Tools tab and the CLI's --strict report, so the three cannot disagree (#1005).
    • Saving a result: on the tool result view, press w to save the result to a file. The prompt opens on <tool-name>-result.json (relative paths resolve against the directory the TUI was launched from); Tab switches between json — the whole result, pretty-printed — and raw — the text of its text blocks, or the decoded bytes of its single binary block — rendered by the shared core/mcp/resultFile.ts, whose encodings match the CLI's planned --output (#2431). Enter writes it and confirms the path; Escape cancels. A failed write is reported on the result view (#2571).
  • Skills: Shown only when the connected server declares the SEP-2640 Skills extension (io.modelcontextprotocol/skills), since it is a server declaration and so only knowable after connecting. The list marks each skill with its structural verdict — ✓ conforms, ! warnings only, ✗ an error — using a glyph as well as a colour, because this pane is read over ssh, in tmux and through script(1). The detail pane shows the entry's URI, description, conformance findings and manifest. Enter verifies the selected skill: one resources/read per manifest file, each hashed against its advertised digest, plus the frontmatter cross-check that compares the served SKILL.md's own frontmatter against the one the listing advertised. v verifies every skill in the listing in one run — the whole catalog, even while a / filter narrows the list — under the server's catalog budget; a line under the list tallies the verdicts held for the current listing (✓ verified, ✗ failed, … stopped by the budget, ? unverifiable, · not yet checked) (#2590). Verification is a gesture rather than a page load because SEP-2640 says hosts MUST NOT retrieve a skill's files ahead of need. The checks are the same ones the web Skills tab and the CLI's --verify run (#2234, #2248).
  • Tasks (s): Shown only when the server supports Tasks (legacy capabilities.tasks or the negotiated SEP-2663 extension). Lists the tasks this client created, with live status. Enter fetches a completed or failed task's result, x cancels a running one, f refreshes and l clears finished tasks. s does not switch here while the Auth pane is focused, where it clears OAuth state (#2432).
  • 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, Skills, 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 / on a focused Resources, Prompts, Skills or Tools list to filter it as you type (case-insensitive, by name or title — and URI, for resources). Enter keeps the filter and returns the keys to the list; Escape clears it. While the filter is being typed, the tab accelerators and Escape-to-exit are paused, so a query can contain any letter. To clear a kept filter, press / then Escape.
  • Press Escape or Ctrl+C to exit the application.
  • Press ? for an in-app list of the keybindings for the current tab; press ? or Escape to close it. The list is the src/utils/keybindings.ts table, so a new keybinding is added there.

Copying values

Any details view (opened from Resources, Prompts, Tools, Protocol or Network) and the Auth tab's access token can be copied out of the TUI (#2421):

  • Y copies the value to your clipboard with an OSC 52 escape sequence. The sequence asks the terminal emulator to set its clipboard, so it reaches the machine you are sitting at even when the TUI runs over SSH. A details view copies the entry's raw JSON; the Auth tab copies the full token, not the truncated prefix it displays.
  • W saves the value to a private temp file (mcp-inspector-copy-*/value.txt, mode 0600) and shows its path — the fallback when OSC 52 does not work. The file is deleted when the TUI exits, since it may hold a token; copy it somewhere else first if you want to keep it.

OSC 52 is fire-and-forget: a terminal that does not support it (or has it disabled) fails silently, which is why the status line after Y points at W. Most modern terminals support it — iTerm2 (enable Applications in terminal may access clipboard), kitty, WezTerm, Alacritty, Windows Terminal, foot, and xterm with allowWindowOps. Inside tmux, set set -g set-clipboard on so tmux forwards the sequence. Some terminals cap the payload (often around 100 KB); a copy above that is flagged in the status line, and W has no such limit.

Development

Like the other clients, the TUI self-validates from its own folder:

npm run check          # format:check && lint && typecheck && build  (no tests)
npm run validate       # check && test
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; the root local:validate (the first stage of npm run local:gate) runs check instead, so the gate runs the suite once, under coverage:tui (#2341). 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 covers all of src/**, React surface included — the Ink components mount through __tests__/helpers/renderTui.tsx, ink-testing-library's render wrapped so every frame it hands back is ANSI-stripped (with the ink-scroll-view / ink-form passthrough doubles in the same directory), App.tsx mounts against a mock of the @inspector/core surface, and keypresses are driven through stdin. The former interim exclusion of the components and App.tsx was lifted in #1501; the only exclusion left in vitest.config.ts is src/tui-servers.ts, a pure re-export of core's server resolver with no runtime statements of its own (its logic is measured in core/ via the web suite, and tui-servers.test.ts still exercises it behaviorally — it is excluded only so it doesn't surface as a misleading 0/0 row).

Import render from __tests__/helpers/renderTui.tsx, never from ink-testing-library directly. Ink writes styling as escape sequences inside the styled run, so an accelerator underline splits the word it decorates — <Text underline>I</Text>nfo reaches the frame buffer with escapes between I and nfo, and expect(frame).toContain("Info") fails against a component that is rendering correctly. Because chalk only emits color when it detects a TTY, this is invisible in CI and hits exactly the developer whose shell exports FORCE_COLOR (#2207). The wrapper strips styling from lastFrame() and frames; the untouched bytes stay available as stdout.lastFrame().

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 beside a React satisfying its peer range, which is looser than ours in every case here:

Package Its react peer Placed beside a different React when…
ink-form, ink-scroll-view ">=18" the consumer has React 18
ink ">=19" the consumer pins React 19.0 (our ^19.2.4 then nests)

Either way the bundle renders through one React while the external package calls hooks on another, whose dispatcher is null — so opening a tool test form (or any scroll view, or in ink's case simply starting the TUI) dies with TypeError: Cannot read properties of null (reading 'useState').

ink-form and ink-scroll-view are therefore inlined, which removes npm from the decision: an inlined package's import "react" is emitted into build/index.js, so it resolves from the build directory exactly like the bundle's own. Inlining also pins transitive deps to what this install resolved, notably ink-select-input@6 via the overrides entry — npm ignores a dependency's overrides, so a consumer install would otherwise pull the React-18-era v5 that ink-form asks for.

Why ink itself is the exception

ink is external for cost, not because its ">=19" peer makes it safe — it does not, and an earlier revision of this section wrongly claimed it did. Bundling ink works (it was built and verified against both consumer repros) but adds ~1.4 MB, since react-reconciler and yoga-layout come with it, plus a createRequire banner: inlined CJS calls require at runtime (react-reconciler for "react", signal-exit@3 for "assert") and esbuild's interop shim throws Dynamic require of "x" is not supported without a real require in scope. A 602 KB bundle beat a 2 MB one.

What makes the exemption tolerable is a separate lever: the root manifest declares react: "^19.0.0" — open to the whole major, rather than pinned at the version we happen to develop against. That lets npm satisfy our React and a consumer's pinned React 19.x with a single copy, so an external ink resolves ours. Verified against a consumer pinning react@19.0.0 alongside ink@6.8.0 — the case that splits under a narrower range:

bundle react   node_modules/react/index.js
ink -> react   node_modules/react/index.js   # same copy

Narrow that range and the exemption turns straight back into #1952, one level up — breaking TUI startup rather than just its forms. tsupConfig.test.ts pins the root range to ink's own peer floor so it can't drift silently. The residual after all this is a React 20-era consumer that also depends on ink; that gets revisited when the Inspector moves to React 20.

__tests__/tsupConfig.test.ts enforces the whole split: every dependency declaring a react peer must be in noExternal unless it is listed as external by design, each exempt package must also be a root dependency (external means consumers install it), and the root react range must stay open to the major. Add a React-rendering dependency, and that test tells you to bundle it.