Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
72 commits
Select commit Hold shift + click to select a range
ae29607
feat(cli,core): shared runner surface for the mcpi session client
BobDickinson Sep 23, 2026
0da0278
feat(mcpi): experimental session CLI client (#1432)
BobDickinson Sep 23, 2026
4853b3b
fix(mcpi): address review security items 1a-1e
BobDickinson Sep 23, 2026
e0e81cb
fix(mcpi): move runtime deps to the root manifest (review 2a)
BobDickinson Sep 23, 2026
8e153cd
test(mcpi): bring ipc-glue and stream-client into the coverage gate (…
BobDickinson Sep 23, 2026
b84feb1
docs(mcpi): record the skills/ tree and link #2461 from the alias not…
BobDickinson Sep 23, 2026
516dffc
docs(mcpi): document per-session container isolation for untrusted st…
BobDickinson Sep 23, 2026
4245080
fix(mcpi): resolve bare stdio command names against the caller's PATH…
BobDickinson Sep 23, 2026
889ed77
feat(daemon-cli): rename mcpi to mcpdo, adopt connection vocabulary, …
BobDickinson Sep 23, 2026
b302636
fix(daemon-cli): address Copilot review round 1 on #1783
BobDickinson Sep 24, 2026
08b133d
fix(daemon-cli): address Copilot review round 2 on #1783
BobDickinson Sep 24, 2026
b484c83
fix(daemon-cli): address Copilot review round 3 on #1783
BobDickinson Sep 24, 2026
e7a1072
fix: address Copilot review round 4 on #1783
BobDickinson Sep 24, 2026
fafb7ce
fix: address Copilot review round 5 on #1783
BobDickinson Sep 24, 2026
ca4ed90
fix(daemon-cli): address Copilot review round 6 on #1783
BobDickinson Sep 24, 2026
6e15ed7
fix: address Copilot review round 7 on #1783
BobDickinson Sep 25, 2026
6c5aef2
fix(daemon-cli): address Copilot review round 8 on #1783
BobDickinson Sep 25, 2026
9fba597
fix(daemon-cli): address Copilot review round 9 on #1783
BobDickinson Sep 25, 2026
2bd7a68
fix(daemon-cli,core): address Copilot review round 10 on #1783
BobDickinson Sep 25, 2026
3166123
fix(daemon-cli): address Copilot review round 11 on #1783
BobDickinson Sep 25, 2026
8b48855
fix(daemon-cli): address Copilot review round 12 on #1783
BobDickinson Sep 25, 2026
f751a2a
fix(daemon-cli): address Copilot review round 13 on #1783
BobDickinson Sep 25, 2026
49d46f9
fix(daemon-cli): address Copilot review round 14 on #1783
BobDickinson Sep 25, 2026
a64e1dd
fix(daemon-cli): address Copilot review round 15 on #1783
BobDickinson Sep 25, 2026
4028c25
fix(daemon-cli, core): address Copilot review round 16 on #1783
BobDickinson Sep 25, 2026
db5e7ab
fix(daemon-cli): address Copilot review round 17 on #1783
BobDickinson Sep 25, 2026
4c1c96d
fix(cli, spec): address Copilot review round 18 on #1783
BobDickinson Sep 25, 2026
9e7e8ad
fix(daemon-cli): address Copilot review round 19 on #1783
BobDickinson Sep 26, 2026
87945d7
fix(daemon-cli, cli): address Copilot review round 20 on #1783
BobDickinson Sep 26, 2026
f87360f
fix(daemon-cli, cli): address Copilot review round 21 on #1783
BobDickinson Sep 26, 2026
b7a675b
fix(daemon-cli, cli): address Copilot review round 22 on #1783
BobDickinson Sep 26, 2026
4012412
docs(daemon-cli): address Copilot review round 23 on #1783
BobDickinson Sep 26, 2026
be7d78e
fix(daemon-cli, cli): address Copilot review round 24 on #1783
BobDickinson Sep 26, 2026
a88e4bd
fix(daemon-cli): address Copilot review round 25 on #1783
BobDickinson Sep 26, 2026
10db834
fix(daemon-cli): address Copilot review round 26 on #1783
BobDickinson Sep 26, 2026
8fc952a
Merge v2/main (64a50d6f) into v2/mcpi-client
BobDickinson Sep 27, 2026
d53f88f
Merge remote-tracking branch 'origin/v2/main' into v2/mcpi-client
BobDickinson Sep 27, 2026
52bdede
fix(daemon-cli): adapt to secret-store token storage after v2/main merge
BobDickinson Sep 27, 2026
7b46097
daemon: transparently revive dropped connections on use
BobDickinson Sep 28, 2026
66df422
mcpdo: show catalog/config provenance and clearer servers/show errors
BobDickinson Sep 28, 2026
6da088c
mcpdo: trigger eval for the shipped skill, and a description that mea…
BobDickinson Sep 28, 2026
7194349
mcpdo: behavior eval — measure the commands agents actually run
BobDickinson Sep 28, 2026
800d51a
eval(mcpdo): per-case server composition and transcript-phase matchers
BobDickinson Sep 28, 2026
64a3ef5
eval(mcpdo): in-process HTTP composed servers and multi-server cases
BobDickinson Sep 28, 2026
7b718fa
mcpdo: non-TTY connect exits with the auth link; detached helper comp…
BobDickinson Sep 28, 2026
8004c06
mcpdo: park elicitations for non-interactive callers
BobDickinson Sep 28, 2026
907f22d
eval(mcpdo): headless OAuth behavior case with an auto-consent user s…
BobDickinson Sep 28, 2026
edbd546
eval(mcpdo): elicitation behavior case with an intrinsic-elicitation …
BobDickinson Sep 29, 2026
71a2b52
mcpdo: batch-update SKILL.md and add skill-gaps regression eval cases
BobDickinson Sep 29, 2026
d848252
mcpdo: reshape agent-help around the skill body
BobDickinson Sep 29, 2026
9baebbf
mcpdo evals: fix two harness OAuth bugs, run both agents, keep failur…
BobDickinson Sep 29, 2026
ea1cf46
mcpdo: restore per-file coverage thresholds with targeted tests
BobDickinson Sep 29, 2026
25395c2
mcpdo evals: spawn agents with a minimal environment, not the develop…
BobDickinson Sep 29, 2026
bca417e
mcpdo evals: assert the helpdesk case's mapped argument values
BobDickinson Sep 29, 2026
4554444
mcpdo evals: prettier fix for skill-eval.test.mjs
BobDickinson Sep 29, 2026
1eacc6b
mcpdo: atomic sign-in flow reservation; gate authUrl OSC 8 links
BobDickinson Sep 29, 2026
d999969
fix(daemon-cli): keep auth wait timer ref'ed and make stale-lock stea…
BobDickinson Sep 29, 2026
8934389
Merge v2/main (1716af17) into v2/mcpi-client
BobDickinson Sep 29, 2026
a5576e3
fix(daemon-cli): pin default env to the caller; fail loud on transcri…
BobDickinson Sep 29, 2026
c3e8823
fix(daemon-cli): JSON Schema code-point lengths, fixture ticket hashi…
BobDickinson Sep 29, 2026
548826c
fix(cli, daemon-cli): buffer early subscribe updates; ownership-safe …
BobDickinson Sep 29, 2026
c385f1d
docs(daemon-cli): describe parked elicitation instead of the former a…
BobDickinson Sep 29, 2026
c4e713a
mcpdo: connections/show completes a finished out-of-band sign-in; lis…
BobDickinson Sep 30, 2026
e3b3722
fix(daemon-cli): override esbuild to ^0.28.2 (GHSA-g7r4-m6w7-qqqr)
BobDickinson Sep 30, 2026
45c45af
fix(daemon-cli): daemon robustness — shutdown, socket decoding, abort…
BobDickinson Sep 30, 2026
a606e39
fix(daemon-cli): keep piped stdin answers across elicitation questions
BobDickinson Sep 30, 2026
2cabc86
fix(daemon-cli): review round-2 follow-ups in terminal output paths
BobDickinson Sep 30, 2026
8250d0b
fix(scripts, docs): eval-harness correctness and shipped-skill guardr…
BobDickinson Sep 30, 2026
138dd59
fix(daemon-cli): park teardown unwires elicitation; ensureDaemon wait…
BobDickinson Sep 30, 2026
3c3de07
feat(scripts): eval pool error containment + end-to-end mcpdo smoke
BobDickinson Sep 30, 2026
bee94ef
fix(scripts, docs): Copilot round-37 findings — Windows env keys, sta…
BobDickinson Oct 1, 2026
205f6f4
docs: URL elicitation has no decline; AGENTS.md says connection CLI
BobDickinson Oct 1, 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
49 changes: 26 additions & 23 deletions .claude/skills/local-dev/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ npm install # at the REPO ROOT
v2 is **not** an npm workspace — each client under `clients/*` keeps its own
`package.json` and `node_modules`. A single root `npm install` 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`.
`npm install` into `clients/web`, `clients/cli`, `clients/daemon-cli`,
`clients/tui`, and `clients/launcher`.

- **Fresh clone:** `npm install` at the root.
- **After a pull that changes a client's dependencies:** re-run `npm install` at
Expand Down Expand Up @@ -52,21 +52,24 @@ The launcher-driven scripts run the **built** launcher, so `npm run build`
first:

```sh
npm run build # web → cli → tui → launcher
npm run build # web → cli → daemon-cli → tui → launcher
npm run web # prod web launcher against clients/web/dist
npm run web:dev # web launcher in --dev mode (Vite)
```

Individual builds: `build:web`, `build:cli`, `build:tui`, `build:launcher`. The
Individual builds: `build:web`, `build:cli`, `build:daemon-cli`, `build:tui`,
`build:launcher`. The
web build produces both the browser SPA (`clients/web/dist`, Vite) and the Node
prod-server runner (`clients/web/build`, tsup).

To run the CLI or TUI: `node clients/launcher/build/index.js --cli …` /
`--tui …`.
`--tui …`. The connection CLI (`mcpdo`) has its own bin:
`node clients/daemon-cli/build/mcp-bin.js …` (or `npm link` from
`clients/daemon-cli` for a global `mcpdo`).

## The `@inspector/core` alias

`core/` holds the logic shared by all three clients and intentionally has **no
`core/` holds the logic shared by all five clients and intentionally has **no
`package.json`** — it is not published on its own. Each client bundles it via a
build-time alias:

Expand All @@ -82,7 +85,7 @@ build-time alias:
## Where a dependency goes

**The rules are in [`AGENTS.md`](../../../AGENTS.md) → Dependency placement, and
they are not restated here.** Read them there and come back for the *why* — what
they are not restated here.** Read them there and come back for the _why_ — what
each rule is defending against, what it looked like when it was violated, and how
to tell you have hit one.

Expand All @@ -107,8 +110,8 @@ Keep two distinctions straight, because AGENTS.md's rules split on them:

- **Root-declared is not the same as `core/`-imported.** `commander`, `open` and
`@hono/node-server` are root `dependencies` too, but they are reached only
from client code. Only the `core/` set has to appear in *all three* bundler
`external` lists.
from client code. Only the `core/` set has to appear in _every_ client's
bundler `external` list.
- **Root-declared is not the same as aliased.** The `vitest.shared.mts` pins and
the `clients/web/tsconfig.*.json` `paths` cover the packages whose resolution
is genuinely ambiguous, which is two different situations: the importer is
Expand Down Expand Up @@ -163,25 +166,25 @@ do not need to be: `npm run` prepends **every ancestor** `node_modules/.bin` to
all still resolves the root's copy. `clients/launcher` declares no
`devDependencies` whatsoever and its `validate` is unchanged.

What a per-client declaration *does* buy is a second copy free to drift, and it
What a per-client declaration _does_ buy is a second copy free to drift, and it
had (#2196): `globals` sat at `^17.7.0` at the root against `^17.4.0` in all four
clients, and `typescript-eslint` at `^8.65.0` against `^8.56.1`. Nothing failed —
which is the point. A lint or format tool that differs per client makes the gate's
verdict a function of *where you ran it*, and the exact `prettier` pin (#1790)
verdict a function of _where you ran it_, and the exact `prettier` pin (#1790)
only means something when there is one of it.

⚠️ The line is **used by every client**, not "used by one" and not "is it
toolchain". `tsx`, `playwright`, `storybook`, `happy-dom`, `ink-testing-library`,
`vite-node` and each client's own `@types/*` are toolchain too and stay where
they are — hoisting them would make every client install the union of all four.
they are — hoisting them would make every client install the union of all five.
So do the ones **more than one** client declares without all of them doing so:
`tsup` sits in web, cli and tui, and `vite` in web and tui on top of the root
*runtime* `dependency` that `--web --dev` needs. Neither is in scope here;
_runtime_ `dependency` that `--web --dev` needs. Neither is in scope here;
whether to consolidate them is a separate call with a separate rationale (`vite`
especially, since its root declaration is a `dependency`, not a
`devDependency`).

#### What the walk-up does *not* buy you
#### What the walk-up does _not_ buy you

⚠️ **Deleting a client's declaration does not always delete the copy** — and
where a copy survives, it is the one that wins. Two mechanisms put one back,
Expand All @@ -195,7 +198,7 @@ neither of which the manifest mentions:
- **A hoisted transitive.** `@types/express` brings `@types/node` into web and
cli's trees on its own.

Those copies sit *nearer* than the root's, so `clients/web/node_modules/.bin`
Those copies sit _nearer_ than the root's, so `clients/web/node_modules/.bin`
precedes the root bin directory on `PATH` and TypeScript resolves the nearest
`node_modules/@types`. Verify with `npm exec -- which eslint` from the client
rather than assuming — the assumption is what made the first cut of #2196 claim
Expand All @@ -217,10 +220,10 @@ on disk. The two mechanisms are **not** equally safe, and neither is a guarantee
#2226.

✅ **`verify:dep-lockstep` gates both of those since #2226.** Its second tier
compares every package **any** install *declares* — `dependencies`,
compares every package **any** install _declares_ — `dependencies`,
`devDependencies` and `optionalDependencies`, unioned across the root and all
four clients — against every **top-level** copy in every install, independent of
what a `tsc` program resolves. So a tool *binary* that no program loads
five clients — against every **top-level** copy in every install, independent of
what a `tsc` program resolves. So a tool _binary_ that no program loads
(`eslint`, `typescript`, `vitest`) and a transitive copy that no single program
meets (the cli `@types/node` above) are both in scope now, as is a skew between
two **clients** with no root copy involved (`@types/react`, web against tui).
Expand Down Expand Up @@ -270,7 +273,7 @@ Two live examples worth knowing:
tsup and Vite externalise what the **client's** `package.json` declares, and a
root-only dependency is in none of them — so it is **bundled**, silently. For a
CJS package inlined into an ESM bundle that is fatal: esbuild leaves a
`Dynamic require of "path" is not supported` shim that throws at *import* time,
`Dynamic require of "path" is not supported` shim that throws at _import_ time,
so the binary dies before it parses a flag (`proper-lockfile`, #2082).

`undici` (#2067) is the worse variant, because it is `import()`ed lazily: the
Expand All @@ -293,7 +296,7 @@ file.
### Why React-rendering packages are the exception

An externalised package resolves its own `react` from wherever npm placed
**it** — beside a React satisfying *that package's* peer range, which is looser
**it** — beside a React satisfying _that package's_ peer range, which is looser
than ours in every case here. `ink-form` and `ink-scroll-view` declare `">=18"`,
so a consumer's React 18 satisfies them and hoists them while our React 19 nests
underneath: the bundle renders through one React, those packages call hooks on
Expand All @@ -303,8 +306,8 @@ another, and the TUI crashes on the first hook (#1952).
a `createRequire` banner). ⚠️ **Never justify that exemption by a peer range** —
it briefly read "its `">=19"` peer keeps npm honest", which is false: a consumer
pinning React 19.0 satisfies `">=19"` while a narrower range of ours nests
underneath. What makes it safe is the *root `react` range staying open to the
whole major*, so npm can dedupe. `clients/tui/__tests__/tsupConfig.test.ts`
underneath. What makes it safe is the _root `react` range staying open to the
whole major_, so npm can dedupe. `clients/tui/__tests__/tsupConfig.test.ts`
enforces the whole split, the exemption included.

### Why a version skew is worth aligning rather than working around
Expand Down Expand Up @@ -332,7 +335,7 @@ an `overrides` entry in that install (see the next section).
### Why `overrides` beats `npm audit fix`

`tsup@8.5.1` declares `esbuild: ^0.27.0`, and the advisory covers
`0.27.3 - 0.28.0` with `0.27.7` the last 0.27.x — so there is no *upward* escape
`0.27.3 - 0.28.0` with `0.27.7` the last 0.27.x — so there is no _upward_ escape
inside that range, and `npm audit fix` "resolves" it by silently **downgrading**
to `0.27.2` across three installs (~700 lines of lockfile churn for a low-severity
dev-only advisory; tried and reverted in #2058). The override forces one deduped
Expand Down
75 changes: 38 additions & 37 deletions .claude/skills/project-structure/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ inspector/
│ │ ├── server/ Node-only dev/prod backend wiring (see below)
│ │ └── static/ sandbox_proxy.html — served for the MCP Apps tab
│ ├── cli/ Scriptable CLI (tsup bundle, @inspector/core alias)
│ ├── daemon-cli/ The `mcpdo` connection CLI bin; daemon + client over local IPC (Unix socket / Windows named pipe; tsup bundle, @inspector/core alias)
│ ├── 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 All @@ -35,41 +36,41 @@ inspector/
Its entry point is the **`InspectorClient`** class, which owns the connection to
an MCP server, the request/response lifecycle, and a set of state stores.

| Directory | Owns |
| --- | --- |
| `core/mcp/` | `InspectorClient`, transports, state stores, config import, URI templates, task/subscription/App-elicitation protocol helpers |
| `core/mcp/node/` | Node stdio transport factory; `proxyFetch.ts` (the shared HTTPS_PROXY/NO_PROXY fetch) |
| `core/mcp/remote/` | Browser HTTP/SSE transport + remote logger/fetch, and (under `node/`) the Hono backend it talks to |
| `core/mcp/state/` | The stores `core/react/` hooks read |
| `core/auth/` | OAuth end to end — providers, discovery, storage, endpoint overrides, scopes, revocation, mid-session recovery — split into isomorphic logic plus `browser/`, `node/` and `remote/` backends |
| `core/auth/node/` | Node OAuth storage + loopback callback server, **and** the `SecretStore` backends (keychain / file / memory) and their selection policy |
| `core/client/` | Install-level client config (`client.json`): browser-safe parse plus Node load/save, remote backend, secrets, runner |
| `core/json/` | JSON + parameter/argument conversion; the schema normalizations all three form builders share (nullable unions, root composition) and the tool-schema portability lint |
| `core/react/` | React hooks over the state stores — consumed by both the web and TUI React trees. Every subscription reads its snapshot **during render** via `useSyncExternalStore` (#1955); `useStoreSnapshot.ts` caches the fresh-value-per-read getters |
| `core/node/` | Node-only helpers: version reader, host normalization/detection |
| `core/storage/` | File I/O helpers used by the OAuth persist backends |
| `core/logging/` | Silent pino logger singleton |
| Directory | Owns |
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `core/mcp/` | `InspectorClient`, transports, state stores, config import, URI templates, task/subscription/App-elicitation protocol helpers |
| `core/mcp/node/` | Node stdio transport factory; `proxyFetch.ts` (the shared HTTPS_PROXY/NO_PROXY fetch) |
| `core/mcp/remote/` | Browser HTTP/SSE transport + remote logger/fetch, and (under `node/`) the Hono backend it talks to |
| `core/mcp/state/` | The stores `core/react/` hooks read |
| `core/auth/` | OAuth end to end — providers, discovery, storage, endpoint overrides, scopes, revocation, mid-session recovery — split into isomorphic logic plus `browser/`, `node/` and `remote/` backends |
| `core/auth/node/` | Node OAuth storage + loopback callback server, **and** the `SecretStore` backends (keychain / file / memory) and their selection policy |
| `core/client/` | Install-level client config (`client.json`): browser-safe parse plus Node load/save, remote backend, secrets, runner |
| `core/json/` | JSON + parameter/argument conversion; the schema normalizations all three form builders share (nullable unions, root composition) and the tool-schema portability lint |
| `core/react/` | React hooks over the state stores — consumed by both the web and TUI React trees. Every subscription reads its snapshot **during render** via `useSyncExternalStore` (#1955); `useStoreSnapshot.ts` caches the fresh-value-per-read getters |
| `core/node/` | Node-only helpers: version reader, host normalization/detection |
| `core/storage/` | File I/O helpers used by the OAuth persist backends |
| `core/logging/` | Silent pino logger singleton |

`core/` is isomorphic (browser + Node) and has **no `package.json`** — it is not
published on its own. Its tests live in `clients/web/src/test/core/`, and its
browser-consumed runtime is inside the web coverage gate.

## `clients/web/server/` — the Node backend

| File | Role |
| --- | --- |
| `vite-hono-plugin.ts` | Hono middleware on the Vite dev server |
| `server.ts` | Standalone Hono prod server |
| `start-vite-dev-server.ts` | In-process Vite starter for the launcher |
| `web-server-config.ts` | Env parsing, initial-config payload, startup banner |
| `sandbox-controller.ts` | The MCP Apps sandbox HTTP server |
| `app-origin-controller.ts` | The dedicated app origin for `_meta.ui.domain` |
| `inject-auth-token.ts` | Embeds the API token into served `index.html` |
| `resolve-bind-host.ts` | Bind-host policy (defaults to `127.0.0.1`; refuses a wildcard bind without the opt-in) |
| `browser-externalized-builtin-gate.ts` | Fails `vite build` on a browser-externalized Node built-in |
| `ensure-web-build.ts` | Builds `clients/web/dist` on demand for prod `--web` |

Each of these files carries a header comment explaining the *why*; read the
| File | Role |
| -------------------------------------- | -------------------------------------------------------------------------------------- |
| `vite-hono-plugin.ts` | Hono middleware on the Vite dev server |
| `server.ts` | Standalone Hono prod server |
| `start-vite-dev-server.ts` | In-process Vite starter for the launcher |
| `web-server-config.ts` | Env parsing, initial-config payload, startup banner |
| `sandbox-controller.ts` | The MCP Apps sandbox HTTP server |
| `app-origin-controller.ts` | The dedicated app origin for `_meta.ui.domain` |
| `inject-auth-token.ts` | Embeds the API token into served `index.html` |
| `resolve-bind-host.ts` | Bind-host policy (defaults to `127.0.0.1`; refuses a wildcard bind without the opt-in) |
| `browser-externalized-builtin-gate.ts` | Fails `vite build` on a browser-externalized Node built-in |
| `ensure-web-build.ts` | Builds `clients/web/dist` on demand for prod `--web` |

Each of these files carries a header comment explaining the _why_; read the
source rather than looking for a second copy of it here.

## Web source layout: `src/lib` vs `src/utils`
Expand All @@ -93,14 +94,14 @@ in near the top of the tree. Element components live in

## Where to put a new file

| It is… | It goes in |
| --- | --- |
| Logic two or more clients need | `core/<area>/` |
| Browser-only React or DOM code | `clients/web/src/` |
| A pure transform used by web | `clients/web/src/utils/` |
| A stateful adapter / subsystem wrapper used by web | `clients/web/src/lib/` |
| Node-only web backend wiring | `clients/web/server/` |
| A build/verify script | `scripts/` (with a sibling `*.test.mjs` if it has pure logic) |
| A test fixture MCP server | `test-servers/src/` + a config in `test-servers/configs/` |
| It is… | It goes in |
| -------------------------------------------------- | ------------------------------------------------------------- |
| Logic two or more clients need | `core/<area>/` |
| Browser-only React or DOM code | `clients/web/src/` |
| A pure transform used by web | `clients/web/src/utils/` |
| A stateful adapter / subsystem wrapper used by web | `clients/web/src/lib/` |
| Node-only web backend wiring | `clients/web/server/` |
| A build/verify script | `scripts/` (with a sibling `*.test.mjs` if it has pure logic) |
| A test fixture MCP server | `test-servers/src/` + a config in `test-servers/configs/` |

Test placement is a separate question with its own rules — see `/testing`.
Loading
Loading