diff --git a/AGENTS.md b/AGENTS.md
index f835e16ef..1ba109fd0 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -214,12 +214,12 @@ gh project item-edit --project-id PVT_kwDOCt2Azc4BJVxt --id "$ITEM_ID" --field-i
- after making the changes, respond to each review comment with what was done (or why it was ignored)
### Mandatory pre-push gate
-- ALWAYS do `npm run format` before committing — the **root** `format` auto-fixes `core/` (`format:core`) and every client's scope in one shot. `validate` runs `format:check` (the non-fixing variant, including `format:check:core`) 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 format` before committing — the **root** `format` auto-fixes `core/` (`format:core`), the root `scripts/**/*.{mjs,js,cjs,ts}` (`format:scripts`), and every client's scope in one shot. `validate` runs `format:check` (the non-fixing variant, including `format:check:core` and `format:check:scripts`) and will fail in CI on any unformatted file, so always run the auto-fixer first rather than letting `format:check` catch it.
- **`npm run ci` is the mandatory pre-push command** — it mirrors `.github/workflows/main.yml` (minus `npm install`): `validate` → `coverage` → `verify:build-gate` (the #1769 browser-externalized-builtin build gate) → `smoke` → Storybook play-function tests (installs Playwright chromium if needed). It now runs **`npm run coverage`**, the per-file ≥90 gate (lines/statements/functions/branches) that CI enforces — so `npm run ci` is a true superset of GitHub CI, and passing it locally means CI's gates will pass. Expect several minutes. **`npm run validate`** remains the fast inner-loop check during development (unit tests only — no coverage gate, no smoke, no Storybook), but it is **NOT** an acceptable substitute for `npm run ci` before pushing: `validate` runs `test`, not `test:coverage`, so it does **zero** coverage gating. Skipping the gate is how a push passes every fast local check and still fails CI (this exact gap broke PR #1601 on a function-coverage regression).
- ALWAYS do `npm run format` before committing, then **`npm run ci`** before pushing. From the repo root, `validate` runs the **`core/` gate first** (`validate:core`) and then chains the four per-client validations (`validate:web` → `validate:cli` → `validate:tui` → `validate:launcher`); each client delegates to its own `npm run validate` 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).
- - **`validate:core` is the shared-code format + lint gate (#1689).** Each client's `prettier`/`eslint` is scoped to its own dir, so nothing reached `core/` before — `validate:core` closes that: it runs `format:check:core` (`prettier --check "core/**/*.{ts,tsx}"`) + `lint:core` (`eslint "core/**/*.{ts,tsx}"` via the **root** `eslint.config.js`). Use `npm run format:core` to auto-fix. The root carries prettier/eslint as devDependencies for this; `core/` is isomorphic (browser + Node globals, no JSX today — the `{ts,tsx}` glob future-proofs against a `core/**/*.tsx`). The root `eslint.config.js` honors an `_`-prefix as the intentionally-unused marker (`argsIgnorePattern`/`varsIgnorePattern`/`caughtErrorsIgnorePattern: '^_'`).
+ - **`validate:core` is the root-owned format + lint gate (#1689, widened in #1778).** Each client's `prettier`/`eslint` is scoped to its own dir, so nothing reached `core/` or the root `scripts/` before — `validate:core` closes that: it runs `format:check:core` (`prettier --check "core/**/*.{ts,tsx}"`) + `format:check:scripts` (`prettier --check "scripts/**/*.{mjs,js,cjs,ts}"`, the root build/verify tooling — #1778; a brace glob so a future non-`.mjs` script is auto-included) + `lint:core` (`eslint "core/**/*.{ts,tsx}"` via the **root** `eslint.config.js`). Use `npm run format:core` / `npm run format:scripts` to auto-fix (both folded into the root `format`). The `scripts/` gate is prettier-only — the root has no eslint config for `.mjs`. The root carries prettier/eslint as devDependencies for this; `core/` is isomorphic (browser + Node globals, no JSX today — the `{ts,tsx}` glob future-proofs against a `core/**/*.tsx`). The root `eslint.config.js` honors an `_`-prefix as the intentionally-unused marker (`argsIgnorePattern`/`varsIgnorePattern`/`caughtErrorsIgnorePattern: '^_'`).
- **cli and tui now typecheck their `src` (#1689).** Their `build`/`test` run through esbuild (no type check), so each has a `typecheck` script (`tsc --noEmit -p tsconfig.json`) folded into `validate`. Their `tsconfig.json` matches `clients/web/tsconfig.app.json`'s module/lib *resolution* options — DOM lib, `moduleResolution: bundler`, and **no** `noUncheckedIndexedAccess` (web's app config does not extend `tsconfig.base`, so re-enabling it would surface `core/` issues web never gates) — so the imported `core/` sources are validated the same way web validates them. It does **not** mirror web's extra strictness flags (`noUnusedLocals`, `verbatimModuleSyntax`, ES2023 target, …), so cli/tui's own `src` is checked slightly more loosely than web's. `core/` itself still typechecks through web's `tsc -b`.
- - 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 && typecheck && 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 && (typecheck &&) build && test`.
+ - 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 && typecheck && 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 && (typecheck &&) build && test`. (#1778) `clients/web`'s `format`/`format:check` covers `src`, `server`, and its top-level configs (`*.{ts,js}` — `vite.config.ts`, `tsup.runner.config.ts`, `eslint.config.js`, …), not just `src`, so the Node backend and Vite/build config are prettier-gated too.
- **`npm run coverage`** is the per-file ≥90 gate and is now part of `npm run ci` — never treat it as optional before a push. It supersedes the old standalone `test:integration` step: web's `test:coverage` runs the `unit` **and** `integration` projects under v8 instrumentation, so `coverage` both enforces the ≥90 gate and exercises the same web integration paths CI covers.
- **`smoke` is NOT part of `validate`** — it is included in `npm run ci`. It runs `smoke:launcher` (`--help` dispatch) plus the prod `smoke:cli` / `smoke:tui` / `smoke:web` / `smoke:web:browser`, 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 `coverage` gate (which also covers the web integration project), then `verify:build-gate` (the #1769 build gate — see below), then `smoke` (with Playwright chromium installed just before it, since `smoke:web:browser` needs it). GitHub CI runs this same chain as separate workflow steps, with the Storybook play-function tests last (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.
diff --git a/README.md b/README.md
index 63fc5cf81..077406f9b 100644
--- a/README.md
+++ b/README.md
@@ -147,7 +147,7 @@ Each client self-validates from its own folder; the root scripts chain them. The
| `npm run ci` | **Mandatory pre-push command.** `validate` → `coverage` → `verify:build-gate` → `smoke` → Storybook. A true superset of GitHub CI. |
| `npm run pack:verify` | Publish smoke — see [Publishing](#publishing). |
-Per-client scripts exist too (`validate:web`, `coverage:cli`, `smoke:tui`, …), plus root `validate:core` / `format:core` for the shared `core/` package. Run `npm run format` before committing — the root `format` fixes `core/` and every client; `validate` runs the non-fixing `format:check` and fails CI on any unformatted file.
+Per-client scripts exist too (`validate:web`, `coverage:cli`, `smoke:tui`, …), plus root `validate:core` / `format:core` for the shared `core/` package and `format:scripts` for the root `scripts/` tooling. Run `npm run format` before committing — the root `format` fixes `core/`, the root `scripts/`, and every client; `validate` runs the non-fixing `format:check` and fails CI on any unformatted file.
For the full testing rules — the ≥90% per-file gate, where test files live, the unit vs. integration vs. storybook projects, and the `v8 ignore` policy — see [`AGENTS.md`](./AGENTS.md).
diff --git a/clients/web/eslint.config.js b/clients/web/eslint.config.js
index 7c72fff48..86602ed31 100644
--- a/clients/web/eslint.config.js
+++ b/clients/web/eslint.config.js
@@ -1,30 +1,35 @@
// For more info, see https://github.com/storybookjs/eslint-plugin-storybook#configuration-flat-config-format
import storybook from "eslint-plugin-storybook";
-import js from '@eslint/js'
-import globals from 'globals'
-import reactHooks from 'eslint-plugin-react-hooks'
-import reactRefresh from 'eslint-plugin-react-refresh'
-import tseslint from 'typescript-eslint'
-import { defineConfig, globalIgnores } from 'eslint/config'
+import js from "@eslint/js";
+import globals from "globals";
+import reactHooks from "eslint-plugin-react-hooks";
+import reactRefresh from "eslint-plugin-react-refresh";
+import tseslint from "typescript-eslint";
+import { defineConfig, globalIgnores } from "eslint/config";
-export default defineConfig([globalIgnores(['dist', 'storybook-static', 'coverage']), {
- files: ['**/*.{ts,tsx}'],
- extends: [
- js.configs.recommended,
- tseslint.configs.recommended,
- reactHooks.configs.flat.recommended,
- reactRefresh.configs.vite,
- ],
- languageOptions: {
- ecmaVersion: 2020,
- globals: globals.browser,
+export default defineConfig([
+ globalIgnores(["dist", "storybook-static", "coverage"]),
+ {
+ files: ["**/*.{ts,tsx}"],
+ extends: [
+ js.configs.recommended,
+ tseslint.configs.recommended,
+ reactHooks.configs.flat.recommended,
+ reactRefresh.configs.vite,
+ ],
+ languageOptions: {
+ ecmaVersion: 2020,
+ globals: globals.browser,
+ },
},
-}, {
- // Test setup files re-export utilities and mix components with helpers — the
- // react-refresh rule does not apply.
- files: ['src/test/**/*.{ts,tsx}', 'src/**/*.test.{ts,tsx}'],
- rules: {
- 'react-refresh/only-export-components': 'off',
+ {
+ // Test setup files re-export utilities and mix components with helpers — the
+ // react-refresh rule does not apply.
+ files: ["src/test/**/*.{ts,tsx}", "src/**/*.test.{ts,tsx}"],
+ rules: {
+ "react-refresh/only-export-components": "off",
+ },
},
-}, ...storybook.configs["flat/recommended"]])
+ ...storybook.configs["flat/recommended"],
+]);
diff --git a/clients/web/package.json b/clients/web/package.json
index 684fa2796..3628f6d51 100644
--- a/clients/web/package.json
+++ b/clients/web/package.json
@@ -26,8 +26,8 @@
"test:integration:watch": "npm run test-servers:build && vitest --project=integration",
"test-servers:build": "tsc -p ../../test-servers --noCheck",
"lint": "eslint .",
- "format": "prettier --write src",
- "format:check": "prettier --check src",
+ "format": "prettier --write src server \"*.{ts,js}\"",
+ "format:check": "prettier --check src server \"*.{ts,js}\"",
"preview": "vite preview",
"storybook": "storybook dev -p 6006"
},
diff --git a/clients/web/tsup.runner.config.ts b/clients/web/tsup.runner.config.ts
index d369dfbcf..863c0e296 100644
--- a/clients/web/tsup.runner.config.ts
+++ b/clients/web/tsup.runner.config.ts
@@ -1,38 +1,38 @@
-import { defineConfig } from 'tsup';
-import path from 'node:path';
-import { fileURLToPath } from 'node:url';
+import { defineConfig } from "tsup";
+import path from "node:path";
+import { fileURLToPath } from "node:url";
const dirname = path.dirname(fileURLToPath(import.meta.url));
-const repoRoot = path.resolve(dirname, '../..');
+const repoRoot = path.resolve(dirname, "../..");
export default defineConfig({
- entry: ['src/index.ts'],
- format: ['esm'],
- outDir: 'build',
+ entry: ["src/index.ts"],
+ format: ["esm"],
+ outDir: "build",
clean: true,
// No source maps in the published bundle — they roughly double the on-disk
// size and aren't needed at runtime (debug via `npm run dev` on the source).
sourcemap: false,
- target: 'node22',
- platform: 'node',
+ target: "node22",
+ platform: "node",
noExternal: [/^@inspector\/core/],
external: [
- 'commander',
- 'open',
- 'pino',
- 'hono',
- '@hono/node-server',
- 'vite',
- '@vitejs/plugin-react',
- 'atomically',
- 'chokidar',
- '@napi-rs/keyring',
- '@modelcontextprotocol/client',
- '@modelcontextprotocol/core',
+ "commander",
+ "open",
+ "pino",
+ "hono",
+ "@hono/node-server",
+ "vite",
+ "@vitejs/plugin-react",
+ "atomically",
+ "chokidar",
+ "@napi-rs/keyring",
+ "@modelcontextprotocol/client",
+ "@modelcontextprotocol/core",
],
esbuildOptions(options) {
options.alias = {
- '@inspector/core': path.join(repoRoot, 'core'),
+ "@inspector/core": path.join(repoRoot, "core"),
};
},
});
diff --git a/clients/web/vite.config.ts b/clients/web/vite.config.ts
index cf2cb775f..21cc39af6 100644
--- a/clients/web/vite.config.ts
+++ b/clients/web/vite.config.ts
@@ -1,20 +1,31 @@
///
-import { defineConfig, type Plugin } from 'vite';
-import react from '@vitejs/plugin-react';
+import { defineConfig, type Plugin } from "vite";
+import react from "@vitejs/plugin-react";
// https://vite.dev/config/
-import path from 'node:path';
-import { fileURLToPath } from 'node:url';
-import { storybookTest } from '@storybook/addon-vitest/vitest-plugin';
-import { playwright } from '@vitest/browser-playwright';
-import { honoMiddlewarePlugin } from './server/vite-hono-plugin';
-import { getViteBaseConfig, getViteDevOptimizeDeps } from './server/vite-base-config';
-import { buildWebServerConfigFromEnv } from './server/web-server-config';
-import { createBrowserExternalizedBuiltinGate } from './server/browser-externalized-builtin-gate';
-import { vitestSharedPaths } from '../../vitest.shared.mts';
-const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url));
-const { repoRoot, sharedDedupe, nodeModulesAliases, projectResolve, sharedAliases } =
- vitestSharedPaths(dirname);
+import path from "node:path";
+import { fileURLToPath } from "node:url";
+import { storybookTest } from "@storybook/addon-vitest/vitest-plugin";
+import { playwright } from "@vitest/browser-playwright";
+import { honoMiddlewarePlugin } from "./server/vite-hono-plugin";
+import {
+ getViteBaseConfig,
+ getViteDevOptimizeDeps,
+} from "./server/vite-base-config";
+import { buildWebServerConfigFromEnv } from "./server/web-server-config";
+import { createBrowserExternalizedBuiltinGate } from "./server/browser-externalized-builtin-gate";
+import { vitestSharedPaths } from "../../vitest.shared.mts";
+const dirname =
+ typeof __dirname !== "undefined"
+ ? __dirname
+ : path.dirname(fileURLToPath(import.meta.url));
+const {
+ repoRoot,
+ sharedDedupe,
+ nodeModulesAliases,
+ projectResolve,
+ sharedAliases,
+} = vitestSharedPaths(dirname);
// Integration tests live under clients/web/src/test/integration/ and run in
// the node-env vitest project below. The folder is the manifest: anything
@@ -26,7 +37,7 @@ const { repoRoot, sharedDedupe, nodeModulesAliases, projectResolve, sharedAliase
// Match `{ts,tsx}` to mirror the unit project's include below — otherwise a
// stray `.test.tsx` placed inside this folder would slip past the integration
// include AND fail to be excluded from unit, silently landing under happy-dom.
-const integrationGlob = 'clients/web/src/test/integration/**/*.test.{ts,tsx}';
+const integrationGlob = "clients/web/src/test/integration/**/*.test.{ts,tsx}";
// Fail `vite build` when a Node built-in lands in the browser bundle (#1769).
// Vite 8 otherwise only *warns* and ships a `module.exports = {}` stub, so a
@@ -55,9 +66,9 @@ const integrationGlob = 'clients/web/src/test/integration/**/*.test.{ts,tsx}';
function browserExternalizedBuiltinGate(): Plugin {
const gate = createBrowserExternalizedBuiltinGate();
return {
- name: 'inspector:fail-on-browser-externalized-builtin',
- apply: 'build',
- applyToEnvironment: (environment) => environment.name === 'client',
+ name: "inspector:fail-on-browser-externalized-builtin",
+ apply: "build",
+ applyToEnvironment: (environment) => environment.name === "client",
// `enforce: 'pre'` runs this ahead of the normal-plugin group (and Vite's
// core plugins) in the `onLog` chain: a plugin whose `onLog` returns `false`
// filters that log for every later plugin, so trailing the array would let a
@@ -66,7 +77,7 @@ function browserExternalizedBuiltinGate(): Plugin {
// position. Harmless to the emitting `rolldown:vite-resolve` plugin — the gate
// defines no resolve/load/transform, and `onLog` delivery doesn't depend on
// the emitter's position.
- enforce: 'pre',
+ enforce: "pre",
buildStart() {
gate.reset();
},
@@ -83,248 +94,248 @@ function browserExternalizedBuiltinGate(): Plugin {
export default defineConfig(({ command }) => {
const isDevServer = command === "serve" && !process.env.VITEST;
return {
- // `honoMiddlewarePlugin` is gated by `apply: 'serve'` so it only attaches
- // during `vite dev` / `vite preview` — vitest projects share this config
- // but never invoke `configureServer`, so the plugin stays inert there.
- //
- // The plugin statically imports the node-only dev backend
- // (`core/mcp/remote/node/server.ts`), so Vite's config bundler (Rolldown)
- // walks that chain when it loads this file and reaches node-only deps
- // (`chokidar`, `atomically`, `@napi-rs/keyring`). Those resolve cleanly at
- // config-bundle time because they're declared in the repo-root
- // `package.json` and installed into the repo-root `node_modules`, which sits
- // on `core/`'s module-resolution chain (core/ has no node_modules of its
- // own). Keep them in the root manifest: drop one and Rolldown can no longer
- // resolve it from core/, reviving the benign `UNRESOLVED_IMPORT` warnings
- // that #1491 eliminated at the source (by removing the old stream filter and
- // build-time onwarn suppressions rather than re-hiding the symptom).
- // `browserExternalizedBuiltinGate` fails `vite build` if a Node built-in
- // reaches the browser bundle (#1769) — see its definition above.
- plugins: [
- react(),
- honoMiddlewarePlugin(buildWebServerConfigFromEnv()),
- browserExternalizedBuiltinGate(),
- ],
- // Shared optimizeDeps exclusions so node-only packages
- // (`@modelcontextprotocol/client/stdio`, `cross-spawn`, `which`)
- // consumed by the dev backend aren't scanned for browser pre-bundling.
- // Browser code reaches the node-side stack via the Hono plugin only.
- // Dev server: force a full dep pre-bundle each launch (no stale cache).
- optimizeDeps: isDevServer
- ? getViteDevOptimizeDeps()
- : getViteBaseConfig().optimizeDeps,
- resolve: {
- // NOTE: the unit vitest project (below) overrides this — see comment there.
+ // `honoMiddlewarePlugin` is gated by `apply: 'serve'` so it only attaches
+ // during `vite dev` / `vite preview` — vitest projects share this config
+ // but never invoke `configureServer`, so the plugin stays inert there.
//
- // Once App.tsx started consuming the full hook + state-manager surface
- // (#1244), the browser dep graph reached bare-module subpaths in core/
- // that Rolldown couldn't resolve against `core/`'s parent (it has no
- // node_modules of its own). Promote the same bare-module aliases the
- // vitest projects use so `vite dev` / `vite build` can resolve them
- // from `clients/web/node_modules`.
- alias: [
- ...Object.entries(sharedAliases).map(([find, replacement]) => ({
- find,
- replacement,
- })),
- ...nodeModulesAliases,
+ // The plugin statically imports the node-only dev backend
+ // (`core/mcp/remote/node/server.ts`), so Vite's config bundler (Rolldown)
+ // walks that chain when it loads this file and reaches node-only deps
+ // (`chokidar`, `atomically`, `@napi-rs/keyring`). Those resolve cleanly at
+ // config-bundle time because they're declared in the repo-root
+ // `package.json` and installed into the repo-root `node_modules`, which sits
+ // on `core/`'s module-resolution chain (core/ has no node_modules of its
+ // own). Keep them in the root manifest: drop one and Rolldown can no longer
+ // resolve it from core/, reviving the benign `UNRESOLVED_IMPORT` warnings
+ // that #1491 eliminated at the source (by removing the old stream filter and
+ // build-time onwarn suppressions rather than re-hiding the symptom).
+ // `browserExternalizedBuiltinGate` fails `vite build` if a Node built-in
+ // reaches the browser bundle (#1769) — see its definition above.
+ plugins: [
+ react(),
+ honoMiddlewarePlugin(buildWebServerConfigFromEnv()),
+ browserExternalizedBuiltinGate(),
],
- // Source files in core/ import bare modules (react, @testing-library/react,
- // etc.) that only exist in clients/web/node_modules. Dedupe ensures Vite
- // resolves them from this package rather than walking up from core/'s
- // location (which has no node_modules of its own yet).
- dedupe: sharedDedupe,
- },
- // Pin the Vite dev server to the same port (and host) the Hono plugin
- // configures from env, so `allowedOrigins` actually matches the browser
- // origin. Without this, `vite dev` falls back to Vite's default 5173
- // while the dev backend's `buildWebServerConfigFromEnv()` defaults to
- // CLIENT_PORT=6274 — origin check rejects every `/api/*` request from
- // the browser. CLIENT_PORT / HOST overrides flow through here too.
- // `strictPort: true` so a port collision fails loudly instead of
- // silently picking a different port (which would leave `allowedOrigins`
- // pointing at the wrong host and break browser fetches).
- server: {
- port: parseInt(process.env.CLIENT_PORT ?? '6274', 10),
- host: process.env.HOST ?? 'localhost',
- strictPort: true,
- fs: {
- allow: [path.resolve(dirname, '../..')],
- },
- },
- test: {
- coverage: {
- provider: 'v8',
- reporter: ['text', 'html', 'json-summary'],
- include: [
- 'src/components/**/*.{ts,tsx}',
- 'src/lib/**/*.{ts,tsx}',
- 'src/utils/**/*.{ts,tsx}',
- 'clients/web/server/**/*.{ts,tsx}',
- path.join(repoRoot, 'core/mcp/**/*.{ts,tsx}'),
- path.join(repoRoot, 'core/json/**/*.{ts,tsx}'),
- path.join(repoRoot, 'core/client/**/*.{ts,tsx}'),
- path.join(repoRoot, 'core/react/**/*.{ts,tsx}'),
- path.join(repoRoot, 'core/auth/**/*.{ts,tsx}'),
- path.join(repoRoot, 'core/storage/**/*.{ts,tsx}'),
- path.join(repoRoot, 'core/logging/**/*.{ts,tsx}'),
- path.join(repoRoot, 'core/node/**/*.{ts,tsx}'),
- ],
- exclude: [
- '**/*.stories.{ts,tsx}',
- '**/*.test.{ts,tsx}',
- '**/*.fixtures.{ts,tsx}',
- '**/index.{ts,tsx}',
- 'src/components/**/types.ts',
- // Dev-backend runtime glue: each file is exercised end-to-end via
- // `npm run dev` (Hono plugin attaches, banner prints, /api/* serves).
- // `vite-hono-plugin.ts` requires standing up a real Vite server with
- // an HTTP listener to drive `configureServer`; `server.ts` is the
- // production Hono entry that the v2 launcher (not yet ported, #1246)
- // will invoke; `start-vite-dev-server.ts` is its dev counterpart.
- // The non-glue parts are extracted to `web-server-config.ts` (fully
- // tested) and `sandbox-controller.ts` (HTTP behavior tested).
- 'clients/web/server/vite-hono-plugin.ts',
- 'clients/web/server/server.ts',
- 'clients/web/server/start-vite-dev-server.ts',
- // Pure-type modules: `interface`/`type` declarations only, no runtime
- // statements. Excluding them keeps the report clean (would otherwise
- // surface as misleading 0/0 rows).
- path.join(repoRoot, 'core/mcp/types.ts'),
- path.join(repoRoot, 'core/mcp/elicitationCreateMessage.ts'),
- path.join(repoRoot, 'core/mcp/samplingCreateMessage.ts'),
- path.join(repoRoot, 'core/mcp/sessionStorage.ts'),
- path.join(repoRoot, 'core/mcp/inspectorClientProtocol.ts'),
- path.join(repoRoot, 'core/mcp/remote/types.ts'),
- path.join(repoRoot, 'core/mcp/import/types.ts'),
- 'clients/web/server/types.ts',
- // .d.ts files are declaration-only.
- path.join(repoRoot, '**/*.d.ts'),
- // inspectorClientEventTarget.ts is types + a single empty-body class
- // (extends TypedEventTarget). v8/istanbul records 0 statements for it
- // today. TODO(#1243): drop this exclusion once the class gains real
- // behavior as the v1.5 InspectorClient port progresses.
- path.join(repoRoot, 'core/mcp/inspectorClientEventTarget.ts'),
- path.join(repoRoot, 'core/mcp/__tests__/**'),
- // test-servers/ is test infrastructure (composable MCP servers and
- // fixtures), not application code — its build output also lives at
- // test-servers/build/, which we don't want to measure either.
- path.join(repoRoot, 'test-servers/**'),
+ // Shared optimizeDeps exclusions so node-only packages
+ // (`@modelcontextprotocol/client/stdio`, `cross-spawn`, `which`)
+ // consumed by the dev backend aren't scanned for browser pre-bundling.
+ // Browser code reaches the node-side stack via the Hono plugin only.
+ // Dev server: force a full dep pre-bundle each launch (no stale cache).
+ optimizeDeps: isDevServer
+ ? getViteDevOptimizeDeps()
+ : getViteBaseConfig().optimizeDeps,
+ resolve: {
+ // NOTE: the unit vitest project (below) overrides this — see comment there.
+ //
+ // Once App.tsx started consuming the full hook + state-manager surface
+ // (#1244), the browser dep graph reached bare-module subpaths in core/
+ // that Rolldown couldn't resolve against `core/`'s parent (it has no
+ // node_modules of its own). Promote the same bare-module aliases the
+ // vitest projects use so `vite dev` / `vite build` can resolve them
+ // from `clients/web/node_modules`.
+ alias: [
+ ...Object.entries(sharedAliases).map(([find, replacement]) => ({
+ find,
+ replacement,
+ })),
+ ...nodeModulesAliases,
],
- thresholds: {
- perFile: true,
- // Uniform 90 per-file gate across every dimension. The branch floor was
- // lifted 50 → 70 (#1271) and then the whole gate raised to 90 after a
- // codebase-wide coverage audit added real tests for every outlier.
- // Genuinely-unreachable branches (Mantine portal / `useMediaQuery` /
- // `typeof window` SSR guards, React StrictMode effect replay, and
- // provably-dead defensive guards) are annotated with justified
- // `/* v8 ignore … */` comments at the source rather than relaxing the
- // gate. New code must clear 90 on all four dimensions.
- lines: 90,
- statements: 90,
- functions: 90,
- branches: 90,
+ // Source files in core/ import bare modules (react, @testing-library/react,
+ // etc.) that only exist in clients/web/node_modules. Dedupe ensures Vite
+ // resolves them from this package rather than walking up from core/'s
+ // location (which has no node_modules of its own yet).
+ dedupe: sharedDedupe,
+ },
+ // Pin the Vite dev server to the same port (and host) the Hono plugin
+ // configures from env, so `allowedOrigins` actually matches the browser
+ // origin. Without this, `vite dev` falls back to Vite's default 5173
+ // while the dev backend's `buildWebServerConfigFromEnv()` defaults to
+ // CLIENT_PORT=6274 — origin check rejects every `/api/*` request from
+ // the browser. CLIENT_PORT / HOST overrides flow through here too.
+ // `strictPort: true` so a port collision fails loudly instead of
+ // silently picking a different port (which would leave `allowedOrigins`
+ // pointing at the wrong host and break browser fetches).
+ server: {
+ port: parseInt(process.env.CLIENT_PORT ?? "6274", 10),
+ host: process.env.HOST ?? "localhost",
+ strictPort: true,
+ fs: {
+ allow: [path.resolve(dirname, "../..")],
},
},
- projects: [
- {
- extends: true,
- // Vitest projects don't inherit `resolve` from the parent. The unit
- // project runs from repoRoot (so vitest's coverage transformer can
- // reach core/), but repoRoot has no node_modules of its own — the
- // shared regex aliases redirect bare `react`/`pino`/etc. imports
- // from core/ back into clients/web/node_modules.
- resolve: projectResolve,
- test: {
- name: 'unit',
- environment: 'happy-dom',
- // Don't let happy-dom actually navigate child frames. Components like
- // the MCP Apps sandbox render an