Skip to content
Merged
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
66 changes: 23 additions & 43 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,50 +22,30 @@ jobs:
# clients/launcher, so this single step sets up every client.
run: npm install

- name: Check formatting
- name: Validate (format, lint, build, fast tests)
# Each client self-validates: format:check + lint + build + test (no
# coverage instrumentation — fast). This also builds every client bundle
# (web dist, cli/tui/launcher) that the smokes below need. The per-file
# coverage gate (`npm run coverage`) is intentionally NOT run in CI — run
# it locally before pushing when you want the gate (#1484).
run: npm run validate

- name: Run web integration tests
# `validate` runs the web UNIT project only (fast); the integration
# project (real stdio/HTTP servers, e2e OAuth, filesystem storage) is
# otherwise only reached via test:coverage. Run it here without coverage
# so CI still exercises those paths even though the gate is local-only.
working-directory: ./clients/web
run: npm run format:check

- name: Run linter
working-directory: ./clients/web
run: npm run lint

- name: Run Build
working-directory: ./clients/web
run: npm run build

- name: Run tests with coverage
working-directory: ./clients/web
run: npm run test:coverage

- name: Run CLI tests with coverage
# In-process runCli() tests under v8 instrumentation + a thin
# out-of-process E2E layer; enforces the per-file gate for
# clients/cli/src (#1484).
working-directory: ./clients/cli
run: npm run test:coverage

- name: Run TUI tests with coverage
# Enforces the per-file gate for the TUI's non-React logic modules
# (interim scope — see clients/tui/vitest.config.ts and #1501).
working-directory: ./clients/tui
run: npm run test:coverage

- name: Build CLI, TUI, and launcher
run: npm run build:cli && npm run build:tui && npm run build:launcher

- name: Smoke test launcher
run: |
node clients/launcher/build/index.js --help
node clients/launcher/build/index.js --cli --help
node clients/launcher/build/index.js --tui --help

- name: Smoke test prod web launcher (end-to-end)
# Starts `mcp-inspector --web` against the built clients/web/dist (from
# the "Run Build" step) and asserts GET / serves the SPA (HTTP 200) with
# the injected auth token — the prod web path the --help smoke can't
# reach (#1486).
run: npm run smoke:web
run: npm run test:integration

- name: Run cross-client smokes
# End-to-end smokes through the built launcher (--help dispatch + prod
# CLI/web). Not part of any client's `validate`: it needs the
# cli/tui/launcher bundles, which `validate` above already built
# (smoke:web builds clients/web/dist on demand — #1486). smoke:tui
# self-skips here — the Ink TUI needs a real TTY (raw mode) that headless
# CI lacks, so its boot/render check is local-only.
run: npm run smoke

- name: Cache Playwright browsers
uses: actions/cache@v4
Expand Down
19 changes: 12 additions & 7 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,10 @@ inspector/
v2 is **not** an npm workspace — each client under `clients/*` keeps its own `package.json` and `node_modules` (see the rationale in [specification/v2_cli_tui_launcher.md](specification/v2_cli_tui_launcher.md)). A single `npm install` at the repo root is still all you need: the root `postinstall` (`scripts/install-clients.mjs`) cascades `npm install` into `clients/web`, `clients/cli`, `clients/tui`, and `clients/launcher`.

- **Fresh clone / first-time setup:** run `npm install` at the repo root.
- **After a pull that changes a client's dependencies:** re-run `npm install` at the root (or `npm run install:clients`) to re-sync every client.
- **After a pull that changes a client's dependencies:** re-run `npm install` at the root to re-sync every client (the `postinstall` cascade handles it).
- The cascade is dev-only: it exits early when the package is installed under `node_modules`, and the published tarball ships only each client's `build/`, so end users are unaffected. Set `INSPECTOR_SKIP_CLIENT_INSTALL=1` to skip it.

After installing, `npm run build` builds all clients. The launcher scripts (`npm run inspector` / `web` / `web:dev`) run the built launcher, so build first; for day-to-day web iteration use `cd clients/web && npm run dev`.
After installing, `npm run build` builds all clients. The launcher scripts (`npm run web` / `web:dev`) run the built launcher, so build first; for day-to-day web iteration use `cd clients/web && npm run dev`.

## Repository & Project Board

Expand Down Expand Up @@ -120,8 +120,9 @@ All work should be driven by items on the project board.
- Run unit tests with `npm run test` (or `npm run test:watch` during development) from `clients/web/`
- Run CLI tests with `npm run test` from `clients/cli/` (builds test-servers + CLI bin first via `pretest`)
- Run TUI tests with `npm run test` from `clients/tui/`
- From repo root, `npm run test` runs web unit tests, then CLI and TUI tests
- Run `npm run test:coverage` to verify the per-file gate: lines ≥ 90, statements ≥ 85, functions ≥ 80, branches ≥ 50 (CI enforces this gate). Branches is intentionally relaxed because Mantine portal/media-query branches are not exercisable under happy-dom; new business-logic branches should still be covered. From the repo root, `npm run test:coverage` runs the gate for web **and** CLI **and** TUI; each client also exposes its own `test:coverage`.
- The repo root has no aggregate `test` script — each client self-validates, so run `npm run validate` from the root (all clients, fast) or `cd clients/<name> && npm run validate` (one client). Each client still exposes its own `test` / `test:coverage` for quick iteration.
- **`validate` is fast: it runs `test`, not `test:coverage`.** The coverage gate (slower — adds v8 instrumentation, and for web the integration project) is a **separate** top-level `npm run coverage` (and per-client `coverage:web` / `coverage:cli` / `coverage:tui` / `coverage:launcher`, each delegating to that client's `test:coverage`). Run `npm run coverage` when you want the gate. **CI does NOT run `coverage`** — the gate is local-only; CI runs `validate` (fast) plus the web integration suite (`clients/web` `test:integration`, no coverage) so the integration paths are still exercised.
- Each client's `test:coverage` enforces a per-file gate: lines ≥ 90, statements ≥ 85, functions ≥ 80, branches ≥ 50 (CI enforces this gate). Branches is intentionally relaxed because Mantine portal/media-query branches are not exercisable under happy-dom; new business-logic branches should still be covered.
- The **same per-file gate** is enforced for the CLI and TUI (#1484), not just web:
- **CLI** (`clients/cli`): tests run **in-process** by importing `runCli()` (see `__tests__/helpers/cli-runner.ts`) so `clients/cli/src` is measured under v8 instrumentation. A thin out-of-process layer (`__tests__/e2e.test.ts` + `scripts/smoke-cli.mjs`) still spawns the built binary for the shebang/`process.exit` paths; `src/index.ts` (binary bootstrap) is the only coverage exclusion. `commander` uses `.exitOverride()` so a parse error throws instead of tearing down the test worker.
- **TUI** (`clients/tui`): the gate covers the **non-React logic** only — `logger.ts`, `components/tabsConfig.ts`, and `utils/*` (server resolution lives in `core/` and is measured by the web suite). The Ink components, `App.tsx`, and `hooks/` are an **interim exclusion** in `clients/tui/vitest.config.ts` pending the renderer-based follow-up (#1501). When adding new **non-React** logic under `clients/tui/src`, it falls under the gate automatically — add tests for it.
Expand All @@ -137,10 +138,14 @@ All work should be driven by items on the project board.

### Lint-fixed, Formatted code
- ALWAYS do `npm run format` before committing — it auto-fixes any Prettier issues. `validate` runs `format:check` (the non-fixing variant) and will fail in CI on any unformatted file, so always run the auto-fixer first rather than letting `format:check` catch it.
- ALWAYS do `npm run validate` before pushing any changes — from the repo root it mirrors what CI enforces across every client: web `format:check` + `lint` + `build` + `test:coverage` (`validate:web`), then the CLI and TUI suites **with their coverage gates** (`test:cli` / `test:tui`, which now run each client's `test:coverage`), then a build of CLI + TUI + launcher, the launcher `--help` smoke (`smoke:launcher`), the prod CLI and TUI end-to-end smokes (`smoke:cli` / `smoke:tui`), and the prod web end-to-end smoke (`smoke:web`) — together `validate:launcher`. The sub-scripts (`validate:web`, `validate:launcher`, `smoke:launcher`, `smoke:cli`, `smoke:tui`, `smoke:web`) can be run individually when iterating on one client. Storybook is the only CI step left out (see below).
- ALWAYS do `npm run validate` before pushing any changes — from the repo root it chains the four per-client validations (`validate:web` → `validate:cli` → `validate:tui` → `validate:launcher`); each delegates to that client's own `npm run validate` = `format:check` + `lint` + `build` + `test` in its own folder (no coverage — fast). Every client is self-validating and the top level just chains them, building each client's bundle along the way (no cross-client build dependencies).
- The one CLI nuance: `clients/cli`'s out-of-process `e2e.test.ts` spawns the built binary, so its `test` **builds first** via `pretest` (`test-servers:build && build`). To avoid building it twice, `clients/cli`'s `validate` folds that in — it is `format:check && lint && test` with **no** separate `build` step (the other clients, whose tests don't spawn their bundle, keep an explicit `build`). `validate:web`/`validate:tui`/`validate:launcher` are the uniform `format:check && lint && build && test`.
- Before pushing, also run **`npm run coverage`** — `validate` is fast and does NOT enforce the per-file gate (or, for web, run the integration project); `coverage` does both. CI does **not** run `coverage` (the gate is local-only); it runs `validate` plus a standalone web `test:integration` step.
- **`smoke` is a separate top-level target, NOT part of `validate`.** Run it (or the individual `smoke:*`) after a build/validate: `npm run validate && npm run smoke`. It runs `smoke:launcher` (`--help` dispatch) plus the prod `smoke:cli` / `smoke:tui` / `smoke:web`, and contains **no build commands** — it assumes the cli/tui/launcher bundles already exist (a full `validate` builds them; `smoke:web` builds `clients/web/dist` on demand). CI runs `validate`, then the web `test:integration` step, then `smoke`. Storybook is the only CI step left out (see below).
- `smoke:launcher` (`scripts/smoke-launcher.mjs`) runs the built launcher with `--help`, `--cli --help`, and `--tui --help`, asserting each exits 0 and prints that mode's usage banner (which also proves the launcher resolved and loaded the right client build). It's the cheap dispatch check before the heavier prod smokes below.
- `smoke:web` (`scripts/smoke-web.mjs`) starts `mcp-inspector --web` (prod, no `--dev`) against the built `clients/web/dist` and asserts `GET /` serves the SPA (HTTP 200) with the injected `__INSPECTOR_API_TOKEN__`. Prod `--web` serves from `clients/web/dist`, which ships in the published package but is absent in a fresh checkout — the runner builds it on demand (`build:client` = `vite build`) on first launch, or exits with an actionable error if that build can't run (see `clients/web/server/ensure-web-build.ts` and the launcher README). `--dev` runs Vite directly and never needs `dist`.
- `smoke:cli` (`scripts/smoke-cli.mjs`) drives `mcp-inspector --cli` through the built launcher against the bundled stdio test server via a temp `--catalog`: it asserts `tools/list` returns the server's tools (real connect over stdio), the default writable catalog is seeded empty on first run, a missing read-only `--config` errors without seeding, and `--catalog` + `--config` is rejected. `smoke:tui` (`scripts/smoke-tui.mjs`) launches `mcp-inspector --tui --catalog <temp>` and asserts the Ink app renders its first frame (the "MCP Servers" panel) within a timeout, then SIGTERMs it — a shallow boot/render check, not full interaction. Both build `test-servers/build` on demand if it's missing.
- Also run `npm run test:storybook` before pushing — it executes every story's `play` function in headless Chromium via `@vitest/browser-playwright` (~10s). CI runs this as a separate step after the unit/lint/build checks; failures block merge. It is kept out of `validate` because it needs the Playwright browser binary and is much slower than the unit suite.
- `smoke:cli` (`scripts/smoke-cli.mjs`) drives `mcp-inspector --cli` through the built launcher against the bundled stdio test server via a temp `--catalog`: it asserts `tools/list` returns the server's tools (real connect over stdio), the default writable catalog is seeded empty on first run, a missing read-only `--config` errors without seeding, and `--catalog` + `--config` is rejected. `smoke:tui` (`scripts/smoke-tui.mjs`) launches `mcp-inspector --tui --catalog <temp>` and asserts the Ink app renders its first frame (the "MCP Servers" panel) within a timeout, then SIGTERMs it — a shallow boot/render check, not full interaction. **`smoke:tui` is local-only: it self-skips when `process.env.CI` is set**, because the Ink TUI needs a real TTY (raw mode) that headless CI lacks — so run it (via `npm run smoke`) on your own machine before pushing. Both build `test-servers/build` on demand if it's missing.
- Also run `npm run test:storybook` from `clients/web/` before pushing — it executes every story's `play` function in headless Chromium via `@vitest/browser-playwright` (~10s). CI runs this as a separate step (from `clients/web`) after `validate`; failures block merge. It is kept out of `validate` because it needs the Playwright browser binary and is much slower than the unit suite. (There is no root-level `test:storybook` aggregate — run it in the web client.)

### Typescript instructions
- Use TypeScript for all new code
Expand Down
14 changes: 11 additions & 3 deletions clients/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,13 +110,21 @@ While the Web Client provides a rich visual interface, the CLI is designed for:

## Development

Run the test suite (and coverage gate) from `clients/cli/`:
Like the other clients, the CLI self-validates from its own folder:

```bash
npm test # build test-servers + binary, then run all tests
npm run test:coverage # same, under the per-file coverage gate
npm run validate # format:check && lint && test:coverage
npm test # build test-servers + binary, then run all tests
npm run test:coverage # build + tests under the per-file coverage gate
```

The CLI's `test:coverage` **builds the binary first** (its out-of-process
`e2e.test.ts` spawns it, so it must run against a fresh build). `validate`
therefore folds the build into `test:coverage` rather than repeating it — it is
`format:check && lint && test:coverage`, with no separate `build` step (the
other clients, whose tests don't spawn their bundle, keep an explicit `build`).
The repo-root `validate:cli` just delegates here.

Tests run the CLI **in-process** (importing `runCli()`) so `src/` is measured
under coverage, with a thin out-of-process spawn layer for the real binary. See
[`__tests__/README.md`](./__tests__/README.md) for details.
19 changes: 19 additions & 0 deletions clients/cli/eslint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import js from '@eslint/js';
import globals from 'globals';
import tseslint from 'typescript-eslint';
import { defineConfig, globalIgnores } from 'eslint/config';

// The CLI is plain Node TypeScript (no React/browser), so this mirrors the web
// client's flat config minus the React/Storybook plugins.
export default defineConfig([
globalIgnores(['build', 'coverage']),
{
files: ['**/*.ts'],
extends: [js.configs.recommended, tseslint.configs.recommended],
languageOptions: {
ecmaVersion: 2022,
sourceType: 'module',
globals: globals.node,
},
},
]);
Loading
Loading