diff --git a/.gitignore b/.gitignore index 949abb829..93d61ce45 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,4 @@ test-servers/build /inspector-network-*.json /*.server.json /configs/ +pr-screenshots diff --git a/AGENTS.md b/AGENTS.md index 1437936c8..bdbb641d7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -194,7 +194,7 @@ gh project item-edit --project-id PVT_kwDOCt2Azc4BJVxt --id "$ITEM_ID" --field-i - Run TUI tests with `npm run test` from `clients/tui/` - 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/ && 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 to reproduce the gate locally before pushing. **CI runs `coverage`** on every push (#1550): the per-file ≥90 gate is CI-enforced, so a PR that drops any file below 90 on lines/statements/functions/branches fails the job. CI runs `validate` (fast) for format/lint/build/unit tests, then `coverage` for the instrumented gate. Because web's `test:coverage` already runs the integration project, CI has no separate `test:integration` step — the integration paths are exercised inside the coverage gate. -- Each client's `test:coverage` enforces a **uniform per-file gate of ≥ 90 on all four dimensions** — lines, statements, functions, and branches — across `clients/web`, `clients/cli`, `clients/tui`, and `clients/launcher` (CI enforces this gate). This is the result of a codebase-wide audit: the branch floor was first lifted 50 → 70 for web (#1271), then the whole gate raised to 90 with real tests added for every outlier. Genuinely-unreachable branches are **not** waved through by lowering the gate — they are annotated at the source with a justified `/* v8 ignore … -- */` comment. Acceptable reasons are happy-dom-inherent paths (Mantine portal mount points, `useMediaQuery` fallbacks, `typeof window` SSR guards), React StrictMode effect-replay blocks, and provably-dead defensive guards (e.g. a `?? fallback` for a value the types guarantee non-null, or a `Select.onChange` receiving a value outside the allowed list). New code must clear 90 on every dimension; reach for a justified `v8 ignore` only when a branch is genuinely impossible to exercise. +- Each client's `test:coverage` enforces a **uniform per-file gate of ≥ 90 on all four dimensions** — lines, statements, functions, and branches — across `clients/web`, `clients/cli`, `clients/tui`, and `clients/launcher` (CI enforces this gate). This is the result of a codebase-wide audit: the branch floor was first lifted 50 → 70 for web (#1271), then the whole gate raised to 90 with real tests added for every outlier. Genuinely-unreachable branches are **not** waved through by lowering the gate — they are annotated at the source with a justified `/* v8 ignore … -- */` comment. Acceptable reasons are happy-dom-inherent paths (Mantine portal mount points, `useMediaQuery` fallbacks, `typeof window` SSR guards), React StrictMode effect-replay blocks, and provably-dead defensive guards (e.g. a `?? fallback` for a value the types guarantee non-null, or a `Select.onChange` receiving a value outside the allowed list). New code must clear 90 on every dimension; reach for a justified `v8 ignore` only when a branch is genuinely impossible to exercise. The web coverage `include` (in `clients/web/vite.config.ts`) covers the shared `core/` runtime consumed by the browser — `core/mcp`, `core/react`, `core/auth`, `core/storage`, `core/logging`, `core/node`, **`core/json`, and `core/client`** (the last two folded in by #1689). When adding a `core/json/*` or `core/client/*` module, its tests live under `clients/web/src/test/core/…` and are gated the same ≥90 way. - 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. @@ -209,10 +209,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 — 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 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. - **`npm run ci` is the mandatory pre-push command** — it mirrors `.github/workflows/main.yml` (minus `npm install`): `validate` → `coverage` → `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` 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`. +- 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: '^_'`). + - **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`. - **`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`, 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 `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. diff --git a/README.md b/README.md index 202f4715a..8b3b2eb1e 100644 --- a/README.md +++ b/README.md @@ -138,15 +138,15 @@ Individual clients: `build:web`, `build:cli`, `build:tui`, `build:launcher`. The Each client self-validates from its own folder; the root scripts chain them. There is **no** aggregate root `test` script — use `validate` (fast) or `coverage` (the gate). -| Script | What it does | -| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `npm run validate` | `format:check` + `lint` + `build` + fast unit tests, per client. The quick inner-loop check. | -| `npm run coverage` | The **per-file ≥90% gate** (lines/statements/functions/branches) under v8 instrumentation, per client. CI-enforced. For web this also runs the integration project. | -| `npm run smoke` | End-to-end smokes through the built launcher (`--help` dispatch + prod cli/tui/web). | -| `npm run ci` | **Mandatory pre-push command.** `validate` → `coverage` → `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`, …). Run `npm run format` (per client) before committing — `validate` runs the non-fixing `format:check` and fails CI on any unformatted file. +| Script | What it does | +| --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `npm run validate` | Runs `validate:core` (the shared `core/` `format:check` + `lint` gate) first, then per client: `format:check` + `lint` + **`typecheck`** (cli/tui only) + `build` + fast unit tests. The quick inner-loop check. | +| `npm run coverage` | The **per-file ≥90% gate** (lines/statements/functions/branches) under v8 instrumentation, per client. CI-enforced. For web this also runs the integration project and covers the shared `core/` runtime (including `core/json` and `core/client`). | +| `npm run smoke` | End-to-end smokes through the built launcher (`--help` dispatch + prod cli/tui/web). | +| `npm run ci` | **Mandatory pre-push command.** `validate` → `coverage` → `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. 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/cli/package.json b/clients/cli/package.json index 05667e5ca..9ef0e9bd8 100644 --- a/clients/cli/package.json +++ b/clients/cli/package.json @@ -16,7 +16,8 @@ ], "scripts": { "build": "tsup", - "validate": "npm run format:check && npm run lint && npm run test", + "typecheck": "tsc --noEmit -p tsconfig.json", + "validate": "npm run format:check && npm run lint && npm run typecheck && npm run test", "test": "vitest run", "test:watch": "vitest", "test:coverage": "npm run test-servers:build && npm run build && vitest run --coverage", diff --git a/clients/cli/src/cli.ts b/clients/cli/src/cli.ts index afb8c1fa5..f63815bca 100644 --- a/clients/cli/src/cli.ts +++ b/clients/cli/src/cli.ts @@ -120,6 +120,20 @@ type MethodOutcome = | { kind: "result"; result: McpResponse; appInfo?: CliAppInfo } | { kind: "emitted" }; +/** + * Tear down a managed list state if it was created for this method call. + * + * The `managed*State` locals below are assigned inside the `runMethod` closure, + * so TypeScript's control-flow analysis keeps them narrowed to `null` at the + * outer `finally` block (a closure "might not have run"). Routing the teardown + * through this helper — whose parameter type is the shared `destroy()` shape — + * both documents that and calls `destroy()` on whatever the closure actually + * assigned at runtime, with no `as` cast. + */ +function destroyManagedState(state: { destroy(): void } | null): void { + state?.destroy(); +} + async function callMethod( serverConfig: MCPServerConfig, serverSettings: InspectorServerSettings | undefined, @@ -370,10 +384,10 @@ async function callMethod( await emitResult(outcome.result, outcome.appInfo, args); } } finally { - managedToolsState?.destroy(); - managedResourcesState?.destroy(); - managedResourceTemplatesState?.destroy(); - managedPromptsState?.destroy(); + destroyManagedState(managedToolsState); + destroyManagedState(managedResourcesState); + destroyManagedState(managedResourceTemplatesState); + destroyManagedState(managedPromptsState); await inspectorClient.disconnect(); } } diff --git a/clients/cli/tsconfig.json b/clients/cli/tsconfig.json index 3f1d5b5e2..522a3ebe2 100644 --- a/clients/cli/tsconfig.json +++ b/clients/cli/tsconfig.json @@ -2,10 +2,24 @@ "extends": "../../tsconfig.base.json", "compilerOptions": { "noEmit": true, + // Match clients/web/tsconfig.app.json's module/lib *resolution* options so + // the imported `core/` sources are validated the same way web's own gate + // validates them: DOM lib + bundler resolution are what core/ (browser-side + // auth, DOM types) expects, and `noUncheckedIndexedAccess` is off because + // web's app config does not extend tsconfig.base — re-enabling it here would + // flag core/ issues web never gates. This 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 src. + "lib": ["ES2023", "DOM", "DOM.Iterable"], + "types": ["node"], + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "module": "ESNext", + "noUncheckedIndexedAccess": false, "paths": { "@inspector/core/*": ["../../core/*"] } }, "include": ["src/**/*"], - "exclude": ["node_modules", "**/*.test.ts", "build"] + "exclude": ["node_modules", "**/*.test.ts", "**/*.test.tsx", "build"] } diff --git a/clients/tui/__tests__/InfoTab.test.tsx b/clients/tui/__tests__/InfoTab.test.tsx index 543b6fde3..275cb586f 100644 --- a/clients/tui/__tests__/InfoTab.test.tsx +++ b/clients/tui/__tests__/InfoTab.test.tsx @@ -11,6 +11,7 @@ import type { MCPServerConfig, ServerState, } from "@inspector/core/mcp/index.js"; +import { headersToServerSettings } from "@inspector/core/mcp/node/servers.js"; import { InfoTab } from "../src/components/InfoTab.js"; // Ink processes stdin keypresses asynchronously — await this after stdin.write @@ -127,14 +128,11 @@ describe("InfoTab", () => { type: "sse", url: "https://example.com/sse", }; - const withHeaders = { - ...config, - headers: { Authorization: "Bearer x" }, - } as unknown as MCPServerConfig; const { lastFrame } = render( { }); it("renders a streamable-http config with headers", () => { - const config = { + const config: MCPServerConfig = { type: "streamable-http", url: "https://example.com/mcp", - headers: { "X-Key": "abc" }, - } as unknown as MCPServerConfig; + }; const { lastFrame } = render( { expect(form.sections[0]?.fields).toEqual([]); }); + it("treats a non-object property value as an empty schema instead of throwing", () => { + // A malformed server schema whose property value is null/primitive must not + // crash — the field degrades to a plain string input labelled by its key. + const form = schemaToForm( + { properties: { bad: null, worse: 42 } }, + "malformed", + ); + const fields = form.sections[0]?.fields ?? []; + expect(fields).toHaveLength(2); + expect( + fields.map((f) => ({ name: f.name, type: f.type, label: f.label })), + ).toEqual([ + { name: "bad", type: "string", label: "bad" }, + { name: "worse", type: "string", label: "worse" }, + ]); + }); + it("maps each JSON Schema type to the matching ink-form field type", () => { const form = schemaToForm( { diff --git a/clients/tui/package.json b/clients/tui/package.json index 265aee96b..67095d5dd 100644 --- a/clients/tui/package.json +++ b/clients/tui/package.json @@ -18,7 +18,8 @@ "scripts": { "dev": "vite-node --config vitest.config.ts dev.ts", "build": "tsup", - "validate": "npm run format:check && npm run lint && npm run build && npm run test", + "typecheck": "tsc --noEmit -p tsconfig.json", + "validate": "npm run format:check && npm run lint && npm run typecheck && npm run build && npm run test", "test": "vitest run", "test:coverage": "vitest run --coverage", "lint": "eslint .", diff --git a/clients/tui/src/App.tsx b/clients/tui/src/App.tsx index 4cb86f1a6..110e97ba4 100644 --- a/clients/tui/src/App.tsx +++ b/clients/tui/src/App.tsx @@ -1660,6 +1660,7 @@ function App({ 0 ? ( + + + Headers:{" "} + {headerPairs.map(({ key, value }) => `${key}=${value}`).join(", ")} + + + ) : null; const scrollViewRef = useRef(null); // Handle keyboard input for scrolling @@ -111,33 +130,13 @@ export function InfoTab({ <> Type: sse URL: {serverConfig.url} - {serverConfig.headers && - Object.keys(serverConfig.headers).length > 0 && ( - - - Headers:{" "} - {Object.entries(serverConfig.headers) - .map(([k, v]) => `${k}=${v}`) - .join(", ")} - - - )} + {headersBlock} ) : serverConfig.type === "streamable-http" ? ( <> Type: streamable-http URL: {serverConfig.url} - {serverConfig.headers && - Object.keys(serverConfig.headers).length > 0 && ( - - - Headers:{" "} - {Object.entries(serverConfig.headers) - .map(([k, v]) => `${k}=${v}`) - .join(", ")} - - - )} + {headersBlock} ) : null} diff --git a/clients/tui/src/utils/schemaToForm.ts b/clients/tui/src/utils/schemaToForm.ts index ac062ef92..bbb730f2c 100644 --- a/clients/tui/src/utils/schemaToForm.ts +++ b/clients/tui/src/utils/schemaToForm.ts @@ -34,9 +34,14 @@ function toSelectOptions( })); } -/** Minimal JSON Schema object shape (properties + required) */ +/** + * Minimal JSON Schema object shape (properties + required). Property values are + * `unknown` so the SDK's broadly-typed `Tool["inputSchema"]` (whose `properties` + * values are the recursive JSON type) is assignable here; each value is narrowed + * to {@link JsonSchemaProperty} at the point of use below. + */ interface JsonSchemaObject { - properties?: Record; + properties?: Record; required?: string[]; } @@ -60,7 +65,12 @@ export function schemaToForm( const required = schema.required || []; for (const [key, prop] of Object.entries(properties)) { - const property = prop as JsonSchemaProperty; + // `properties` values are `unknown` (the SDK schema admits anything), so + // guard before treating a value as a schema object — a malformed server + // schema with e.g. `properties: { foo: null }` must not throw on `.title`. + const property = ( + typeof prop === "object" && prop !== null ? prop : {} + ) as JsonSchemaProperty; const baseField = { name: key, label: property.title || key, diff --git a/clients/tui/tsconfig.json b/clients/tui/tsconfig.json index 6f42313b1..1d5ac9cac 100644 --- a/clients/tui/tsconfig.json +++ b/clients/tui/tsconfig.json @@ -1,13 +1,30 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { + "noEmit": true, "jsx": "react-jsx", - "outDir": "./build", - "rootDir": ".", + // Match clients/web/tsconfig.app.json's module/lib *resolution* options so + // the imported `core/` sources are validated the same way web's own gate + // validates them: DOM lib + bundler resolution are what core/ (browser-side + // auth, DOM types) expects, `noUncheckedIndexedAccess` is off because web's + // app config does not extend tsconfig.base (re-enabling it would flag core/ + // issues web never gates), and the `react` path redirect lets core/ source + // resolve its bare `react` import from this client's node_modules. This does + // NOT mirror web's extra strictness flags (noUnusedLocals, verbatimModule- + // Syntax, ES2023 target, …), so tui's own src is checked slightly more + // loosely than web's src. + "lib": ["ES2023", "DOM", "DOM.Iterable"], + "types": ["node"], + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "module": "ESNext", + "noUncheckedIndexedAccess": false, "paths": { - "@inspector/core/*": ["../../core/*"] + "@inspector/core/*": ["../../core/*"], + "react": ["./node_modules/@types/react"], + "react/jsx-runtime": ["./node_modules/@types/react/jsx-runtime"] } }, "include": ["index.ts", "tui.tsx", "src/**/*"], - "exclude": ["node_modules", "**/*.test.ts", "build"] + "exclude": ["node_modules", "**/*.test.ts", "**/*.test.tsx", "build"] } diff --git a/clients/web/package-lock.json b/clients/web/package-lock.json index 3fa91d91f..027df5286 100644 --- a/clients/web/package-lock.json +++ b/clients/web/package-lock.json @@ -69,7 +69,7 @@ "globals": "^17.4.0", "happy-dom": "^20.9.0", "playwright": "^1.58.2", - "prettier": "^3.8.1", + "prettier": "^3.8.4", "storybook": "^10.2.19", "tsup": "^8.5.1", "typescript": "~5.9.3", @@ -8192,9 +8192,9 @@ } }, "node_modules/prettier": { - "version": "3.8.1", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz", - "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", + "version": "3.8.4", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.4.tgz", + "integrity": "sha512-N2MylSdi48+5N/6S5j+maeHbUSIzzZ5uOcX5Hm4QpV8Dkb1HFjfAKTKX6yNPJQD9AhcT3ifHNB66tWTTJDi11Q==", "dev": true, "license": "MIT", "bin": { diff --git a/clients/web/src/test/core/client/config.test.ts b/clients/web/src/test/core/client/config.test.ts index cc0f795ab..bb2b7712c 100644 --- a/clients/web/src/test/core/client/config.test.ts +++ b/clients/web/src/test/core/client/config.test.ts @@ -10,16 +10,23 @@ import { import { CLIENT_KEYCHAIN_ID, extractSecretsFromClientConfig, + hasClientPlaintextSecret, mergeSecretsIntoClientConfig, } from "@inspector/core/client/secrets.js"; import { + getClientConfigFilePath, loadClientConfig, parseClientConfig, saveClientConfig, } from "@inspector/core/client/config.js"; import { + CIMD_METADATA_URL_HTTPS_ERROR, + CIMD_METADATA_URL_INVALID_ERROR, + CIMD_METADATA_URL_PATH_ERROR, formatClientConfigLoadError, + getCimdClientMetadataUrlError, isAbsoluteHttpUrl, + serializeClientConfig, } from "@inspector/core/client/config-parse.js"; import { getActiveCimdClientMetadataUrl, @@ -210,6 +217,13 @@ describe("client config", () => { ).toThrow(/path/); }); + it("getClientConfigFilePath honors a custom path and defaults to the storage dir", () => { + expect(getClientConfigFilePath("/custom/client.json")).toBe( + "/custom/client.json", + ); + expect(getClientConfigFilePath()).toMatch(/client\.json$/); + }); + it("loadClientConfig returns {} when file is absent", async () => { tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "client-config-")); const filePath = path.join(tmpDir, "client.json"); @@ -217,6 +231,76 @@ describe("client config", () => { expect(config).toEqual({}); }); + it("saveClientConfig/loadClientConfig round-trip without a secretStore", async () => { + tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "client-config-")); + const filePath = path.join(tmpDir, "client.json"); + const input = { + cimd: { + enabled: true, + clientMetadataUrl: "https://example.com/oauth/client.json", + }, + }; + await saveClientConfig(input, { filePath }); + expect(await loadClientConfig({ filePath })).toEqual(input); + }); + + it("hasClientPlaintextSecret reflects the presence of an IdP clientSecret", () => { + expect( + hasClientPlaintextSecret({ + enterpriseManagedAuth: { + idp: { issuer: "https://idp.example.com", clientId: "c" }, + }, + }), + ).toBe(false); + expect( + hasClientPlaintextSecret({ + enterpriseManagedAuth: { + idp: { + issuer: "https://idp.example.com", + clientId: "c", + clientSecret: "s", + }, + }, + }), + ).toBe(true); + }); + + it("getActiveCimdClientMetadataUrl returns undefined for a whitespace-only URL", () => { + expect( + getActiveCimdClientMetadataUrl({ + cimd: { enabled: true, clientMetadataUrl: " " }, + }), + ).toBeUndefined(); + }); + + it("serializeClientConfig emits pretty-printed JSON", () => { + expect(serializeClientConfig({ cimd: { clientMetadataUrl: "x" } })).toBe( + JSON.stringify({ cimd: { clientMetadataUrl: "x" } }, null, 2), + ); + }); + + describe("getCimdClientMetadataUrlError", () => { + it("returns undefined for an empty value or a valid https URL with a path", () => { + expect(getCimdClientMetadataUrlError("")).toBeUndefined(); + expect(getCimdClientMetadataUrlError(" ")).toBeUndefined(); + expect( + getCimdClientMetadataUrlError("https://example.com/oauth/client.json"), + ).toBeUndefined(); + }); + + it("flags unparseable, non-https, and pathless URLs distinctly", () => { + expect(getCimdClientMetadataUrlError("not-a-url")).toBe( + CIMD_METADATA_URL_INVALID_ERROR, + ); + expect( + getCimdClientMetadataUrlError("http://example.com/oauth/client.json"), + ).toBe(CIMD_METADATA_URL_HTTPS_ERROR); + expect(getCimdClientMetadataUrlError("https://example.com")).toBe( + CIMD_METADATA_URL_PATH_ERROR, + ); + }); + }); + it("formatClientConfigLoadError summarizes Zod validation failures", () => { try { parseClientConfig({ diff --git a/clients/web/src/test/core/client/node-persistence.test.ts b/clients/web/src/test/core/client/node-persistence.test.ts new file mode 100644 index 000000000..c00559257 --- /dev/null +++ b/clients/web/src/test/core/client/node-persistence.test.ts @@ -0,0 +1,179 @@ +import { describe, it, expect, afterEach } from "vitest"; +import * as fs from "node:fs/promises"; +import { existsSync, readFileSync } from "node:fs"; +import * as os from "node:os"; +import * as path from "node:path"; +import { + InMemorySecretStore, + KeychainUnavailableError, + SECRET_FIELD_IDP_CLIENT_SECRET, + type SecretStore, +} from "@inspector/core/auth/node/secret-store.js"; +import { CLIENT_KEYCHAIN_ID } from "@inspector/core/client/secrets.js"; +import { + deleteClientConfigStore, + readClientConfigStore, + writeClientConfigStore, +} from "@inspector/core/client/node-persistence.js"; + +const configWithPlaintextSecret = { + enterpriseManagedAuth: { + idp: { + issuer: "https://idp.example.com", + clientId: "cid", + clientSecret: "plain", + }, + }, +}; + +describe("client node-persistence", () => { + let tmpDir: string; + + afterEach(async () => { + if (tmpDir) { + await fs.rm(tmpDir, { recursive: true, force: true }); + } + }); + + async function makeTmpFile(contents?: string): Promise { + tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "client-persist-")); + const filePath = path.join(tmpDir, "client.json"); + if (contents !== undefined) { + await fs.writeFile(filePath, contents, "utf-8"); + } + return filePath; + } + + it("migrates a plaintext secret to an empty keychain and strips it from disk", async () => { + const filePath = await makeTmpFile( + JSON.stringify(configWithPlaintextSecret), + ); + const secretStore = new InMemorySecretStore(); + + const loaded = await readClientConfigStore(filePath, secretStore); + + // Rehydrated result still carries the secret (read back from the keychain). + expect(loaded.enterpriseManagedAuth?.idp.clientSecret).toBe("plain"); + // On-disk copy is stripped. + expect(readFileSync(filePath, "utf-8")).not.toContain("plain"); + // Keychain now holds it. + expect( + await secretStore.get(CLIENT_KEYCHAIN_ID, SECRET_FIELD_IDP_CLIENT_SECRET), + ).toBe("plain"); + }); + + it("does not overwrite an existing keychain secret during migration", async () => { + const filePath = await makeTmpFile( + JSON.stringify(configWithPlaintextSecret), + ); + const secretStore = new InMemorySecretStore(); + await secretStore.set( + CLIENT_KEYCHAIN_ID, + SECRET_FIELD_IDP_CLIENT_SECRET, + "existing", + ); + + const loaded = await readClientConfigStore(filePath, secretStore); + + // The keychain value wins over the disk plaintext. + expect(loaded.enterpriseManagedAuth?.idp.clientSecret).toBe("existing"); + expect( + await secretStore.get(CLIENT_KEYCHAIN_ID, SECRET_FIELD_IDP_CLIENT_SECRET), + ).toBe("existing"); + // Disk is still stripped. + expect(readFileSync(filePath, "utf-8")).not.toContain("plain"); + }); + + it("keeps the plaintext secret on disk when the keychain is unavailable", async () => { + const filePath = await makeTmpFile( + JSON.stringify(configWithPlaintextSecret), + ); + // A store whose writes always fail as if libsecret were missing. + const unavailable: SecretStore = { + async get() { + return null; + }, + async set() { + throw new KeychainUnavailableError(new Error("no libsecret")); + }, + async delete() {}, + async deleteAllForServer() {}, + }; + + const loaded = await readClientConfigStore(filePath, unavailable); + + // Migration bailed → original config (with the secret) is returned and the + // on-disk copy is left untouched (still contains the plaintext). + expect(loaded.enterpriseManagedAuth?.idp.clientSecret).toBe("plain"); + expect(readFileSync(filePath, "utf-8")).toContain("plain"); + }); + + it("rethrows a non-keychain error raised during migration", async () => { + const filePath = await makeTmpFile( + JSON.stringify(configWithPlaintextSecret), + ); + const boom: SecretStore = { + async get() { + return null; + }, + async set() { + throw new Error("disk on fire"); + }, + async delete() {}, + async deleteAllForServer() {}, + }; + + await expect(readClientConfigStore(filePath, boom)).rejects.toThrow( + /disk on fire/, + ); + }); + + it("returns {} when the client.json file is absent", async () => { + const filePath = await makeTmpFile(); + expect( + await readClientConfigStore(filePath, new InMemorySecretStore()), + ).toEqual({}); + }); + + it("deletes the keychain secret when writing a config without one", async () => { + const filePath = await makeTmpFile(); + const secretStore = new InMemorySecretStore(); + await secretStore.set( + CLIENT_KEYCHAIN_ID, + SECRET_FIELD_IDP_CLIENT_SECRET, + "stale", + ); + + await writeClientConfigStore( + filePath, + { + cimd: { enabled: true, clientMetadataUrl: "https://x.example/c.json" }, + }, + secretStore, + ); + + expect( + await secretStore.get(CLIENT_KEYCHAIN_ID, SECRET_FIELD_IDP_CLIENT_SECRET), + ).toBeNull(); + expect(readFileSync(filePath, "utf-8")).toContain("clientMetadataUrl"); + }); + + it("deleteClientConfigStore removes both the file and the keychain secret", async () => { + const filePath = await makeTmpFile( + JSON.stringify({ cimd: { enabled: false, clientMetadataUrl: "" } }), + ); + const secretStore = new InMemorySecretStore(); + await secretStore.set( + CLIENT_KEYCHAIN_ID, + SECRET_FIELD_IDP_CLIENT_SECRET, + "gone", + ); + + await deleteClientConfigStore(filePath, secretStore); + + expect(existsSync(filePath)).toBe(false); + expect( + await secretStore.get(CLIENT_KEYCHAIN_ID, SECRET_FIELD_IDP_CLIENT_SECRET), + ).toBeNull(); + }); +}); diff --git a/clients/web/src/test/core/client/remote.test.ts b/clients/web/src/test/core/client/remote.test.ts new file mode 100644 index 000000000..617da1de8 --- /dev/null +++ b/clients/web/src/test/core/client/remote.test.ts @@ -0,0 +1,176 @@ +import { describe, it, expect, vi, afterEach } from "vitest"; +import { + loadClientConfigRemote, + saveClientConfigRemote, +} from "@inspector/core/client/remote.js"; +import type { ClientConfig } from "@inspector/core/client/types.js"; + +afterEach(() => { + vi.unstubAllGlobals(); +}); + +const validConfig: ClientConfig = { + enterpriseManagedAuth: { + idp: { issuer: "https://idp.example.com", clientId: "cid" }, + }, +}; + +/** + * Build a minimal `Response`-like stub for the mocked fetch. The code under + * test only reads `.ok`, `.status`, and `.json()`, so the stub implements just + * those. The double cast is unavoidable here: `Response` has ~20 required + * members (`headers`, `body`, `arrayBuffer`, `blob`, `clone`, …), so a direct + * `as Response` is a TS2352 error and there is no lighter alternative than + * asserting through `unknown` for a deliberately-partial test double. + */ +function fakeResponse(init: { + ok: boolean; + status: number; + json?: () => Promise; +}): Response { + return { + ok: init.ok, + status: init.status, + json: init.json ?? (async () => ({})), + } as unknown as Response; +} + +describe("client remote config", () => { + describe("loadClientConfigRemote", () => { + it("parses a populated store and strips the trailing slash from baseUrl", async () => { + const fetchFn = vi.fn(async () => + fakeResponse({ ok: true, status: 200, json: async () => validConfig }), + ); + const config = await loadClientConfigRemote({ + baseUrl: "http://localhost:3000/", + fetchFn, + }); + expect(config).toEqual(validConfig); + expect(fetchFn).toHaveBeenCalledWith( + "http://localhost:3000/api/storage/client", + expect.objectContaining({ method: "GET" }), + ); + }); + + it("sends the bearer auth header when a token is provided", async () => { + const fetchFn = vi.fn(async () => + fakeResponse({ ok: true, status: 200, json: async () => ({}) }), + ); + await loadClientConfigRemote({ + baseUrl: "http://localhost:3000", + authToken: "tok", + fetchFn, + }); + expect(fetchFn).toHaveBeenCalledWith( + "http://localhost:3000/api/storage/client", + expect.objectContaining({ + headers: { "x-mcp-remote-auth": "Bearer tok" }, + }), + ); + }); + + it("returns {} on a 404", async () => { + const fetchFn = vi.fn(async () => + fakeResponse({ ok: false, status: 404 }), + ); + expect( + await loadClientConfigRemote({ baseUrl: "http://x", fetchFn }), + ).toEqual({}); + }); + + it("throws on a non-404 error status", async () => { + const fetchFn = vi.fn(async () => + fakeResponse({ ok: false, status: 500 }), + ); + await expect( + loadClientConfigRemote({ baseUrl: "http://x", fetchFn }), + ).rejects.toThrow(/Failed to read client config: 500/); + }); + + it("returns {} when the store is empty or not an object", async () => { + const emptyObj = vi.fn(async () => + fakeResponse({ ok: true, status: 200, json: async () => ({}) }), + ); + expect( + await loadClientConfigRemote({ + baseUrl: "http://x", + fetchFn: emptyObj, + }), + ).toEqual({}); + + const nullJson = vi.fn(async () => + fakeResponse({ ok: true, status: 200, json: async () => null }), + ); + expect( + await loadClientConfigRemote({ + baseUrl: "http://x", + fetchFn: nullJson, + }), + ).toEqual({}); + }); + }); + + describe("saveClientConfigRemote", () => { + it("POSTs the validated, serialized config with content-type and auth headers", async () => { + const fetchFn = vi.fn(async () => + fakeResponse({ ok: true, status: 200 }), + ); + await saveClientConfigRemote(validConfig, { + baseUrl: "http://localhost:3000/", + authToken: "tok", + fetchFn, + }); + expect(fetchFn).toHaveBeenCalledWith( + "http://localhost:3000/api/storage/client", + expect.objectContaining({ + method: "POST", + headers: { + "Content-Type": "application/json", + "x-mcp-remote-auth": "Bearer tok", + }, + body: JSON.stringify(validConfig, null, 2), + }), + ); + }); + + it("omits the auth header when no token is given", async () => { + const fetchFn = vi.fn(async () => + fakeResponse({ ok: true, status: 200 }), + ); + await saveClientConfigRemote(validConfig, { + baseUrl: "http://x", + fetchFn, + }); + expect(fetchFn).toHaveBeenCalledWith( + "http://x/api/storage/client", + expect.objectContaining({ + headers: { "Content-Type": "application/json" }, + }), + ); + }); + + it("throws when the write fails", async () => { + const fetchFn = vi.fn(async () => + fakeResponse({ ok: false, status: 503 }), + ); + await expect( + saveClientConfigRemote(validConfig, { baseUrl: "http://x", fetchFn }), + ).rejects.toThrow(/Failed to write client config: 503/); + }); + }); + + describe("global fetch fallback", () => { + it("uses globalThis.fetch when no fetchFn is supplied", async () => { + const globalFetch = vi.fn(async () => + fakeResponse({ ok: true, status: 200, json: async () => validConfig }), + ); + vi.stubGlobal("fetch", globalFetch); + + const loaded = await loadClientConfigRemote({ baseUrl: "http://x" }); + expect(loaded).toEqual(validConfig); + + await saveClientConfigRemote(validConfig, { baseUrl: "http://x" }); + expect(globalFetch).toHaveBeenCalledTimes(2); + }); + }); +}); diff --git a/clients/web/src/test/core/client/runner.test.ts b/clients/web/src/test/core/client/runner.test.ts index 87b3d62df..2b3ee568d 100644 --- a/clients/web/src/test/core/client/runner.test.ts +++ b/clients/web/src/test/core/client/runner.test.ts @@ -1,7 +1,12 @@ -import { describe, it, expect } from "vitest"; +import { describe, it, expect, afterEach, vi } from "vitest"; +import * as fs from "node:fs/promises"; +import * as os from "node:os"; +import * as path from "node:path"; +import { InMemorySecretStore } from "@inspector/core/auth/node/secret-store.js"; import { buildRunnerClientAuthOptions, isOAuthCapableServerConfig, + loadRunnerClientConfig, } from "@inspector/core/client/runner.js"; import type { ClientConfig } from "@inspector/core/client/types.js"; import type { InspectorServerSettings } from "@inspector/core/mcp/types.js"; @@ -80,4 +85,82 @@ describe("runner client auth options", () => { expect(opts.oauth?.enterpriseManaged).toBe(true); expect(opts.oauth?.clientId).toBe("resource-client"); }); + + it("buildRunnerClientAuthOptions returns no oauth when nothing supplies it", () => { + expect(buildRunnerClientAuthOptions({})).toEqual({}); + }); + + it("buildRunnerClientAuthOptions wires CLI client id/secret and marks directAuthRecovery", () => { + const opts = buildRunnerClientAuthOptions({}, undefined, { + clientId: "cli-id", + clientSecret: "cli-secret", + }); + expect(opts.oauth?.clientId).toBe("cli-id"); + expect(opts.oauth?.clientSecret).toBe("cli-secret"); + expect(opts.directAuthRecovery).toBe(true); + }); + + it("buildRunnerClientAuthOptions carries oauth scopes and client secret from server settings", () => { + const settings: InspectorServerSettings = { + oauthClientSecret: "resource-secret", + oauthScopes: "read write", + requestTimeout: 0, + connectionTimeout: 0, + taskTtl: 60000, + maxFetchRequests: 10, + autoRefreshOnListChanged: false, + metadata: [], + headers: [], + env: [], + roots: [], + }; + const opts = buildRunnerClientAuthOptions({}, settings); + expect(opts.oauth?.clientSecret).toBe("resource-secret"); + expect(opts.oauth?.scope).toBe("read write"); + }); +}); + +describe("loadRunnerClientConfig", () => { + let tmpDir: string; + + afterEach(async () => { + if (tmpDir) { + await fs.rm(tmpDir, { recursive: true, force: true }); + } + vi.unstubAllEnvs(); + }); + + it("reads client.json from an explicit path (empty when absent)", async () => { + tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "runner-client-")); + const filePath = path.join(tmpDir, "client.json"); + // No injected store here → exercises the default `new KeyringSecretStore()` + // path (its get() tolerates keychain unavailability, returning {}). + expect( + await loadRunnerClientConfig({ clientConfigPath: filePath }), + ).toEqual({}); + }); + + it("falls back to MCP_CLIENT_CONFIG_PATH and parses a stored config", async () => { + tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "runner-client-")); + const filePath = path.join(tmpDir, "client.json"); + await fs.writeFile( + filePath, + JSON.stringify({ + cimd: { + enabled: true, + clientMetadataUrl: "https://example.com/oauth/client.json", + }, + }), + "utf-8", + ); + vi.stubEnv("MCP_CLIENT_CONFIG_PATH", filePath); + // Inject an in-memory store so the result never depends on the developer's + // real OS keychain. + const config = await loadRunnerClientConfig({ + secretStore: new InMemorySecretStore(), + }); + expect(config.cimd?.clientMetadataUrl).toBe( + "https://example.com/oauth/client.json", + ); + }); }); diff --git a/clients/web/src/test/core/jsonUtils.test.ts b/clients/web/src/test/core/jsonUtils.test.ts index 89f25f0c8..df422cada 100644 --- a/clients/web/src/test/core/jsonUtils.test.ts +++ b/clients/web/src/test/core/jsonUtils.test.ts @@ -3,15 +3,38 @@ import { convertParameterValue, convertToolParameters, convertPromptArguments, + toRecord, } from "@inspector/core/json/jsonUtils.js"; import type { Tool } from "@modelcontextprotocol/client"; describe("JSON Utils", () => { + describe("toRecord", () => { + it("returns the same object widened to a string-keyed record", () => { + const source = { a: 1, b: "two" }; + const widened = toRecord(source); + expect(widened).toBe(source); + expect(widened.a).toBe(1); + expect(Object.keys(widened)).toEqual(["a", "b"]); + }); + }); + describe("convertParameterValue", () => { it("should convert string to string", () => { expect(convertParameterValue("hello", { type: "string" })).toBe("hello"); }); + it("returns the raw value unchanged when it is empty", () => { + // Empty string short-circuits before any type coercion. + expect(convertParameterValue("", { type: "number" })).toBe(""); + }); + + it("falls back to the raw string when JSON parsing fails", () => { + expect(convertParameterValue("{not json", { type: "object" })).toBe( + "{not json", + ); + expect(convertParameterValue("[oops", { type: "array" })).toBe("[oops"); + }); + it("should convert string to number", () => { expect(convertParameterValue("42", { type: "number" })).toBe(42); expect(convertParameterValue("3.14", { type: "number" })).toBe(3.14); diff --git a/clients/web/vite.config.ts b/clients/web/vite.config.ts index 78e8a639c..126031d34 100644 --- a/clients/web/vite.config.ts +++ b/clients/web/vite.config.ts @@ -105,7 +105,8 @@ export default defineConfig(({ command }) => { 'src/lib/**/*.{ts,tsx}', 'clients/web/server/**/*.{ts,tsx}', path.join(repoRoot, 'core/mcp/**/*.{ts,tsx}'), - path.join(repoRoot, 'core/json/xMcpHeader.ts'), + 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}'), diff --git a/core/auth/challenge.ts b/core/auth/challenge.ts index 631e61601..c30074e6e 100644 --- a/core/auth/challenge.ts +++ b/core/auth/challenge.ts @@ -65,11 +65,7 @@ export class AuthChallengeError extends Error { readonly authChallenge: AuthChallenge; readonly status: number; - constructor( - authChallenge: AuthChallenge, - status: number, - message?: string, - ) { + constructor(authChallenge: AuthChallenge, status: number, message?: string) { super(message ?? `Auth challenge: ${authChallenge.reason}`); this.name = "AuthChallengeError"; this.authChallenge = authChallenge; @@ -262,8 +258,7 @@ export function parseAuthChallengeFromError( } const status = - (err as { status?: number }).status ?? - (err as { code?: number }).code; + (err as { status?: number }).status ?? (err as { code?: number }).code; if (status !== 401 && status !== 403) { return undefined; } @@ -271,8 +266,9 @@ export function parseAuthChallengeFromError( const wwwAuthenticate = authChallenge?.raw?.wwwAuthenticate ?? (err as { wwwAuthenticate?: string }).wwwAuthenticate ?? - (err as { headers?: { get?: (name: string) => string | null } }).headers - ?.get?.("WWW-Authenticate") ?? + ( + err as { headers?: { get?: (name: string) => string | null } } + ).headers?.get?.("WWW-Authenticate") ?? undefined; if (!wwwAuthenticate?.length) { @@ -309,7 +305,8 @@ export function isAuthChallengeError(err: unknown): boolean { return false; } - const authChallenge = (err as { authChallenge?: AuthChallenge }).authChallenge; + const authChallenge = (err as { authChallenge?: AuthChallenge }) + .authChallenge; if (authChallenge?.reason) { return true; } @@ -323,8 +320,9 @@ export function isAuthChallengeError(err: unknown): boolean { const wwwAuthenticate = authChallenge?.raw?.wwwAuthenticate ?? (err as { wwwAuthenticate?: string }).wwwAuthenticate ?? - (err as { headers?: { get?: (name: string) => string | null } }).headers - ?.get?.("WWW-Authenticate") ?? + ( + err as { headers?: { get?: (name: string) => string | null } } + ).headers?.get?.("WWW-Authenticate") ?? undefined; return wwwAuthenticate !== undefined && wwwAuthenticate.length > 0; diff --git a/core/auth/ema/index.ts b/core/auth/ema/index.ts index 77fc34ccc..e5e5a847e 100644 --- a/core/auth/ema/index.ts +++ b/core/auth/ema/index.ts @@ -22,7 +22,10 @@ export { normalizeIdpIssuer, type EmaIdpLoginState, } from "./idpSession.js"; -export { discoverEmaResourceContext, resolveEmaScopes } from "./resourceContext.js"; +export { + discoverEmaResourceContext, + resolveEmaScopes, +} from "./resourceContext.js"; export { EmaClientNotConfiguredError, emaClientNotConfiguredMessage, diff --git a/core/auth/index.ts b/core/auth/index.ts index e9e2e80d5..cf82840d0 100644 --- a/core/auth/index.ts +++ b/core/auth/index.ts @@ -25,13 +25,14 @@ export type { BuildOAuthConnectionStateParams } from "./connection-state.js"; export { ensureCimdClientRegistration } from "./cimd.js"; export { mcpAuth, type McpAuthOptions, type McpAuthResult } from "./mcpAuth.js"; -export { - computeScopeUnion, - isStrictScopeSuperset, -} from "./scopes.js"; +export { computeScopeUnion, isStrictScopeSuperset } from "./scopes.js"; // Storage -export type { OAuthStorage, IdpSessionState, SaveClientInformationOptions } from "./storage.js"; +export type { + OAuthStorage, + IdpSessionState, + SaveClientInformationOptions, +} from "./storage.js"; export { getServerSpecificKey, OAUTH_STORAGE_KEYS } from "./storage.js"; // Providers diff --git a/core/auth/node/index.ts b/core/auth/node/index.ts index b7526f206..647d9beea 100644 --- a/core/auth/node/index.ts +++ b/core/auth/node/index.ts @@ -1,7 +1,4 @@ -export { - NodeOAuthStorage, - clearAllOAuthClientState, -} from "./storage-node.js"; +export { NodeOAuthStorage, clearAllOAuthClientState } from "./storage-node.js"; export { createOAuthCallbackServer, OAuthCallbackServer, @@ -21,9 +18,7 @@ export { parseRunnerOAuthCallbackUrl, } from "./runner-oauth-callback.js"; export type { RunnerOAuthCallbackConfig } from "./runner-oauth-callback.js"; -export { - runRunnerInteractiveOAuth, -} from "./runner-interactive-oauth.js"; +export { runRunnerInteractiveOAuth } from "./runner-interactive-oauth.js"; export type { RunRunnerInteractiveOAuthOptions, RunnerInteractiveOAuthClient, diff --git a/core/auth/node/runner-oauth-callback.ts b/core/auth/node/runner-oauth-callback.ts index cb53eeb9d..e8a374a3f 100644 --- a/core/auth/node/runner-oauth-callback.ts +++ b/core/auth/node/runner-oauth-callback.ts @@ -32,9 +32,7 @@ export function parseRunnerOAuthCallbackUrl( cliCallbackUrl?: string, ): RunnerOAuthCallbackConfig { const raw = - cliCallbackUrl?.trim() || - process.env.MCP_OAUTH_CALLBACK_URL?.trim() || - ""; + cliCallbackUrl?.trim() || process.env.MCP_OAUTH_CALLBACK_URL?.trim() || ""; if (!raw) { return { hostname: RUNNER_OAUTH_CALLBACK_DEFAULT_HOSTNAME, diff --git a/core/auth/node/secret-store.ts b/core/auth/node/secret-store.ts index 4bc8c2181..dbc07d791 100644 --- a/core/auth/node/secret-store.ts +++ b/core/auth/node/secret-store.ts @@ -13,10 +13,7 @@ * browser side never imports this; it gets values rehydrated into the * `/api/servers` response by the Hono handler. */ -import { - AsyncEntry, - findCredentialsAsync, -} from "@napi-rs/keyring"; +import { AsyncEntry, findCredentialsAsync } from "@napi-rs/keyring"; const SERVICE_NAME = "mcp-inspector"; diff --git a/core/auth/oauthUx.ts b/core/auth/oauthUx.ts index df7fdc26e..da2737223 100644 --- a/core/auth/oauthUx.ts +++ b/core/auth/oauthUx.ts @@ -84,8 +84,7 @@ export function isStepUpConfirmation( options?: { enterpriseManaged?: boolean }, ): boolean { return ( - isStandardOAuthStepUp(challenge, options) || - isEmaStepUp(challenge, options) + isStandardOAuthStepUp(challenge, options) || isEmaStepUp(challenge, options) ); } @@ -124,9 +123,7 @@ export function stepUpFollowUpMessage(options?: { export function stepUpAuthorizeActionLabel(options?: { enterpriseManaged?: boolean; }): string { - return options?.enterpriseManaged - ? "Authorize" - : "Authorize (opens browser)"; + return options?.enterpriseManaged ? "Authorize" : "Authorize (opens browser)"; } export function stepUpModalTitle(options?: { @@ -198,7 +195,9 @@ export function oauthPreRedirectToastCopy( const name = options.serverName; if (authKind === "step_up") { return { - title: name ? `Step-up authorization for "${name}"` : "Step-up authorization", + title: name + ? `Step-up authorization for "${name}"` + : "Step-up authorization", message: "Redirecting to authorize additional permissions…", }; } diff --git a/core/client/config-parse.ts b/core/client/config-parse.ts index 361e4c703..82c66f1f9 100644 --- a/core/client/config-parse.ts +++ b/core/client/config-parse.ts @@ -71,7 +71,9 @@ export const CIMD_METADATA_URL_PATH_ERROR = * Returns undefined when the value is valid; empty strings are not flagged here * (required-field gating lives in {@link canPersistClientSettingsDraft}). */ -export function getCimdClientMetadataUrlError(value: string): string | undefined { +export function getCimdClientMetadataUrlError( + value: string, +): string | undefined { const trimmed = value.trim(); if (trimmed === "") return undefined; if (!isAbsoluteHttpUrl(trimmed)) { diff --git a/core/client/remote.ts b/core/client/remote.ts index 589150695..344651c56 100644 --- a/core/client/remote.ts +++ b/core/client/remote.ts @@ -2,10 +2,7 @@ * Remote HTTP load/save for client.json via /api/storage/client. */ -import { - parseClientConfig, - serializeClientConfig, -} from "./config-parse.js"; +import { parseClientConfig, serializeClientConfig } from "./config-parse.js"; import type { ClientConfig } from "./types.js"; export interface RemoteClientConfigOptions { diff --git a/core/client/runner.ts b/core/client/runner.ts index 41bc12368..e61a461a2 100644 --- a/core/client/runner.ts +++ b/core/client/runner.ts @@ -3,8 +3,14 @@ * for Node runners (TUI, CLI). */ -import { KeyringSecretStore } from "../auth/node/secret-store.js"; -import type { InspectorClientOptions, InspectorServerSettings } from "../mcp/types.js"; +import { + KeyringSecretStore, + type SecretStore, +} from "../auth/node/secret-store.js"; +import type { + InspectorClientOptions, + InspectorServerSettings, +} from "../mcp/types.js"; import { loadClientConfig } from "./config.js"; import type { ClientConfig } from "./types.js"; import { @@ -15,6 +21,9 @@ import { export interface LoadRunnerClientConfigOptions { /** Explicit path from `--client-config` (or MCP_CLIENT_CONFIG_PATH when unset). */ clientConfigPath?: string; + /** Secret store for the IdP clientSecret; defaults to the OS keychain. Tests + * inject an in-memory store for determinism. */ + secretStore?: SecretStore; } /** Load install-level client.json with keychain-backed IdP secrets. */ @@ -25,7 +34,7 @@ export async function loadRunnerClientConfig( options?.clientConfigPath?.trim() || process.env.MCP_CLIENT_CONFIG_PATH?.trim() || undefined; - const secretStore = new KeyringSecretStore(); + const secretStore = options?.secretStore ?? new KeyringSecretStore(); return loadClientConfig({ filePath: customPath, secretStore }); } @@ -53,7 +62,10 @@ export function buildRunnerClientAuthOptions( cliOverrides?: RunnerClientConfigOverrides, ): Pick< InspectorClientOptions, - "oauth" | "enterpriseManagedAuth" | "installEnterpriseManagedAuth" | "directAuthRecovery" + | "oauth" + | "enterpriseManagedAuth" + | "installEnterpriseManagedAuth" + | "directAuthRecovery" > { const activeIdp = getActiveEnterpriseManagedAuthIdp(clientConfig); const activeCimdUrl = getActiveCimdClientMetadataUrl(clientConfig); @@ -84,9 +96,7 @@ export function buildRunnerClientAuthOptions( cliOverrides?.clientMetadataUrl?.trim() || activeCimdUrl; const oauthFromCli = - cliOverrides?.clientId || - cliOverrides?.clientSecret || - clientMetadataUrl + cliOverrides?.clientId || cliOverrides?.clientSecret || clientMetadataUrl ? { ...(cliOverrides?.clientId && { clientId: cliOverrides.clientId }), ...(cliOverrides?.clientSecret && { diff --git a/core/client/types.ts b/core/client/types.ts index 9c37b5351..ff02f3402 100644 --- a/core/client/types.ts +++ b/core/client/types.ts @@ -28,9 +28,7 @@ export interface ClientConfig { } /** True when install-level EMA IdP config is active (not just stored). */ -export function isEnterpriseManagedAuthEnabled( - config: ClientConfig, -): boolean { +export function isEnterpriseManagedAuthEnabled(config: ClientConfig): boolean { const ema = config.enterpriseManagedAuth; if (!ema?.idp) return false; return ema.enabled !== false; diff --git a/core/mcp/config.ts b/core/mcp/config.ts index 808055470..4b7c3270a 100644 --- a/core/mcp/config.ts +++ b/core/mcp/config.ts @@ -32,9 +32,7 @@ export function isOAuthCapableServerType(type: ServerType): boolean { * MCP server URL used as the OAuth storage key (includes path, for discovery). * Undefined for stdio transports. */ -export function getOAuthServerUrl( - config: MCPServerConfig, -): string | undefined { +export function getOAuthServerUrl(config: MCPServerConfig): string | undefined { if (config.type === "sse" || config.type === "streamable-http") { return config.url; } diff --git a/core/mcp/inspectorClient.ts b/core/mcp/inspectorClient.ts index b29fab8a1..8a4d6705c 100644 --- a/core/mcp/inspectorClient.ts +++ b/core/mcp/inspectorClient.ts @@ -1827,9 +1827,12 @@ export class InspectorClient extends InspectorClientEventTarget { // a future modern-family revision would negotiate a different string, and // the two must not disagree. The raw channel only runs on a connected modern // session, so this is always set; the constant is a defensive fallback. - /* v8 ignore next -- fallback only if getProtocolVersion() is unset, which - can't happen on the connected modern session this runs on. */ - const protocolVersion = this.getProtocolVersion() ?? MODERN_PROTOCOL_VERSION; + /* v8 ignore start -- fallback only if getProtocolVersion() is unset, which + can't happen on the connected modern session this runs on. Bracketed so + the ignore is reflow-proof however prettier splits the statement. */ + const protocolVersion = + this.getProtocolVersion() ?? MODERN_PROTOCOL_VERSION; + /* v8 ignore stop */ return { ...params, _meta: { @@ -1861,9 +1864,7 @@ export class InspectorClient extends InspectorClientEventTarget { ...message, result: { resultType: "complete", - content: [ - { type: "text", text: `Modern task ${task.taskId} created` }, - ], + content: [{ type: "text", text: `Modern task ${task.taskId} created` }], _meta: { [MODERN_TASK_HANDLE_META]: task }, }, }; @@ -1906,7 +1907,9 @@ export class InspectorClient extends InspectorClientEventTarget { const raw = await new Promise((resolve, reject) => { const timer = setTimeout(() => { this.pendingRawWireRequests.delete(id); - reject(new Error(`Raw request "${method}" timed out after ${timeoutMs} ms`)); + reject( + new Error(`Raw request "${method}" timed out after ${timeoutMs} ms`), + ); }, timeoutMs); this.pendingRawWireRequests.set(id, { resolve, reject, timer }); transport.send(message).catch((err: unknown) => { @@ -4443,7 +4446,10 @@ export class InspectorClient extends InspectorClientEventTarget { this.modernReconnectTimer = undefined; // Disconnect/unsubscribe may have raced the timer — bail if the reconnect // is no longer wanted. - if (isTerminalStatus(this.status) || this.subscribedResources.size === 0) { + if ( + isTerminalStatus(this.status) || + this.subscribedResources.size === 0 + ) { return; } this.refreshModernSubscription(true).catch(() => diff --git a/core/mcp/node/authChallengeFetch.ts b/core/mcp/node/authChallengeFetch.ts index 00361a395..af5dcb2ca 100644 --- a/core/mcp/node/authChallengeFetch.ts +++ b/core/mcp/node/authChallengeFetch.ts @@ -32,4 +32,4 @@ export function createAuthChallengeInterceptFetch( `MCP auth challenge (${response.status})`, ); }; -} \ No newline at end of file +} diff --git a/core/mcp/node/server-secrets.ts b/core/mcp/node/server-secrets.ts index 4d883fc50..aae0bd141 100644 --- a/core/mcp/node/server-secrets.ts +++ b/core/mcp/node/server-secrets.ts @@ -1,8 +1,5 @@ import type { SecretStore } from "../../auth/node/secret-store.js"; -import { - expectedSecretFields, - mergeSecretsIntoStored, -} from "../serverList.js"; +import { expectedSecretFields, mergeSecretsIntoStored } from "../serverList.js"; import type { MCPConfig } from "../types.js"; /** diff --git a/core/mcp/state/managedRequestorTasksState.ts b/core/mcp/state/managedRequestorTasksState.ts index a75d28d48..93b584f17 100644 --- a/core/mcp/state/managedRequestorTasksState.ts +++ b/core/mcp/state/managedRequestorTasksState.ts @@ -72,7 +72,10 @@ export class ManagedRequestorTasksState extends TypedEventTarget + const shouldIgnoreUpdate = ( + taskId: string, + status: Task["status"], + ): boolean => this.dismissedTaskIds.has(taskId) || (this.cancelledTaskIds.has(taskId) && status !== "cancelled"); const onTaskStatusChange = ( @@ -200,7 +203,9 @@ export class ManagedRequestorTasksState extends TypedEventTarget { + private async refreshModern( + client: InspectorClientProtocol, + ): Promise { const ids = this.tasks .map((t) => t.taskId) .filter((id) => !this.dismissedTaskIds.has(id)); diff --git a/core/mcp/state/resourceSubscriptionsState.ts b/core/mcp/state/resourceSubscriptionsState.ts index ab57e4214..c26b95edb 100644 --- a/core/mcp/state/resourceSubscriptionsState.ts +++ b/core/mcp/state/resourceSubscriptionsState.ts @@ -25,7 +25,10 @@ import type { InspectorResourceSubscription, ResourceSubscriptionStreamState, } from "../types.js"; -import { isTerminalStatus, INACTIVE_SUBSCRIPTION_STREAM_STATE } from "../types.js"; +import { + isTerminalStatus, + INACTIVE_SUBSCRIPTION_STREAM_STATE, +} from "../types.js"; import type { Resource } from "@modelcontextprotocol/client"; import { TypedEventTarget, diff --git a/core/mcp/types.ts b/core/mcp/types.ts index 92b7ad3f1..d190dd15d 100644 --- a/core/mcp/types.ts +++ b/core/mcp/types.ts @@ -583,8 +583,7 @@ export const MODERN_LOG_LEVELS: ModernLogLevel[] = [ /** Runtime guard for the {@link ModernLogLevel} literal (hand-edited files). */ export function isModernLogLevel(value: unknown): value is ModernLogLevel { return ( - typeof value === "string" && - (MODERN_LOG_LEVELS as string[]).includes(value) + typeof value === "string" && (MODERN_LOG_LEVELS as string[]).includes(value) ); } diff --git a/core/react/useClientSettingsDraft.ts b/core/react/useClientSettingsDraft.ts index be682376d..648f6790b 100644 --- a/core/react/useClientSettingsDraft.ts +++ b/core/react/useClientSettingsDraft.ts @@ -74,9 +74,7 @@ export function useClientSettingsDraft({ const prev = latestValuesRef.current; if (prev === null) return; const resolved = - typeof next === "function" - ? (next as (prev: T) => T)(prev) - : next; + typeof next === "function" ? (next as (prev: T) => T)(prev) : next; latestValuesRef.current = resolved; setDraft(resolved); if (timerRef.current) clearTimeout(timerRef.current); diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 000000000..4db86dbe0 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,38 @@ +import js from "@eslint/js"; +import globals from "globals"; +import tseslint from "typescript-eslint"; +import { defineConfig, globalIgnores } from "eslint/config"; + +// Root-level lint gate for the shared `core/` package. Each client's own +// `eslint .` is scoped to its own directory, so nothing reached `core/` before +// (#1689) — this config closes that gap. `core/` is isomorphic TypeScript +// (browser-side OAuth + Node backends + shared runtime), so both browser and +// Node globals apply; there is no JSX in `core/`, so no React plugin is needed. +export default defineConfig([ + globalIgnores(["core/**/build/**", "core/**/dist/**"]), + { + files: ["core/**/*.{ts,tsx}"], + extends: [js.configs.recommended, tseslint.configs.recommended], + languageOptions: { + ecmaVersion: 2022, + sourceType: "module", + globals: { + ...globals.node, + ...globals.browser, + }, + }, + rules: { + // An `_`-prefix is `core/`'s explicit "intentionally unused" marker — + // interface-conformance params in fakes, destructuring-rest omissions, + // and reserved-for-later args. Honor it rather than deleting signal. + "@typescript-eslint/no-unused-vars": [ + "error", + { + argsIgnorePattern: "^_", + varsIgnorePattern: "^_", + caughtErrorsIgnorePattern: "^_", + }, + ], + }, + }, +]); diff --git a/package-lock.json b/package-lock.json index ba22f189c..22e5af663 100644 --- a/package-lock.json +++ b/package-lock.json @@ -37,6 +37,14 @@ "bin": { "mcp-inspector": "clients/launcher/build/index.js" }, + "devDependencies": { + "@eslint/js": "^9.39.5", + "eslint": "^9.39.5", + "globals": "^17.7.0", + "prettier": "^3.8.4", + "typescript": "~5.9.3", + "typescript-eslint": "^8.65.0" + }, "engines": { "node": ">=22.19.0" } @@ -85,6 +93,187 @@ "tslib": "^2.4.0" } }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", + "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.5" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.6.tgz", + "integrity": "sha512-l2Ul9PrHsPCKcEY/ac7VgFj9D80C7S68sOKc618SyHDPK36s1XcFebXY0iTzUVn4Yq+YbwvSnDmCz9yxjX+QrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.14.0", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.3.0", + "minimatch": "^3.1.5", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@eslint/js": { + "version": "9.39.5", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.5.tgz", + "integrity": "sha512-QywQuszQh77pIXCsq998c8hbhSTI/azTty1Z6N53dmAudKHhy573j3yvRLsX2BSp8YpLtoCEG8E9DJe+8zUh4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, "node_modules/@hono/node-server": { "version": "1.19.14", "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.14.tgz", @@ -97,6 +286,72 @@ "hono": "^4" } }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, "node_modules/@modelcontextprotocol/client": { "version": "2.0.0-beta.5", "resolved": "https://registry.npmjs.org/@modelcontextprotocol/client/-/client-2.0.0-beta.5.tgz", @@ -753,6 +1008,302 @@ "tslib": "^2.4.0" } }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.65.0.tgz", + "integrity": "sha512-IEgob78X12rHpUmtcwFsXhZdVGJtwTVP8FiCLZkR6GlYVrl2PcuB+KhCE5BlVC/eQpQnu8WXRtkHZuPar+gCRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.65.0", + "@typescript-eslint/type-utils": "8.65.0", + "@typescript-eslint/utils": "8.65.0", + "@typescript-eslint/visitor-keys": "8.65.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.65.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.6.tgz", + "integrity": "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.65.0.tgz", + "integrity": "sha512-CZ4nMxWwgu1HEEFNkeaCptra9QCtkmKdgf3sWh1rl1trIhmxLilgTV4cwcbQ4wemnT4sWQN8CaKOmdYx+g2gMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.65.0", + "@typescript-eslint/types": "8.65.0", + "@typescript-eslint/typescript-estree": "8.65.0", + "@typescript-eslint/visitor-keys": "8.65.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.65.0.tgz", + "integrity": "sha512-SxnPhbTsGahizDgbu7oqFH/xVtzIqMd/s+WtnSxNxJZJpLbdT5IPdzg8EZxO3+PoKahXmwJLeNQOpKJb3/bi7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.65.0", + "@typescript-eslint/types": "^8.65.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.65.0.tgz", + "integrity": "sha512-Esbl8OSYiVxBokYgWPf7VVWg/BE798wXhimnn9ML9Pt5qoDf8bfQlgjlKXR/k98+AcNzlLKYrpCcrcuZ9DZLgg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.65.0", + "@typescript-eslint/visitor-keys": "8.65.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.65.0.tgz", + "integrity": "sha512-j6GzGqCiRdA7Qhur2VVmKZAkBLfnHFQfx4TaJGL9RMveZqCo48jSHHO0DTgizEnGhtWnqmbtCUSrqSkdiY/0Hg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.65.0.tgz", + "integrity": "sha512-YjaZ7PRI5qY7ax2L3PbvX0rRyGtipAReCWs0mhhDBHjH/vl0g0BonaGXrKdKpMbIIsMIwDgbk/xzkBTyAltS5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.65.0", + "@typescript-eslint/typescript-estree": "8.65.0", + "@typescript-eslint/utils": "8.65.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.65.0.tgz", + "integrity": "sha512-JSSwWNy+H0E/01jJEM+hrX6N0OFDzFzeIhHFSAS01tlVaevpG8cFyYRPhS5yjGOvBUx3sqQHVMjCL1CAZZMxBg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.65.0.tgz", + "integrity": "sha512-JboAE2swaYt4tb1fHhHTABE2K+OLy09XfcTbhnk4Pw96f9dd2e9iYsJ28gBggHlo5z5x1rkyWvcPoTuNTd4oGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.65.0", + "@typescript-eslint/tsconfig-utils": "8.65.0", + "@typescript-eslint/types": "8.65.0", + "@typescript-eslint/visitor-keys": "8.65.0", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.8.tgz", + "integrity": "sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.65.0.tgz", + "integrity": "sha512-gXiwIHsYreboxeJucHKPvgwl7dXt50mF8s1/c00cP/WoVTyWKFdtfhRWwZiXYFU5H2O8vVoSLNrexFZjYS/SGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.65.0", + "@typescript-eslint/types": "8.65.0", + "@typescript-eslint/typescript-estree": "8.65.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.65.0.tgz", + "integrity": "sha512-8C71BQkGjiMmXtop7pHVJu1l2NNShFdkCyD6a2ezzs5vU/L3LRtb69EtcteFwz0mYMPzIgOw0n6OV4VBUWZd7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.65.0", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/@vitejs/plugin-react": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.2.tgz", @@ -792,6 +1343,29 @@ "node": ">= 0.6" } }, + "node_modules/acorn": { + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", + "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, "node_modules/ajv": { "version": "8.18.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", @@ -865,6 +1439,13 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, "node_modules/atomic-sleep": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", @@ -896,6 +1477,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, "node_modules/body-parser": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", @@ -921,6 +1509,17 @@ "url": "https://opencollective.com/express" } }, + "node_modules/brace-expansion": { + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz", + "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, "node_modules/bundle-name": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", @@ -976,6 +1575,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/chalk": { "version": "5.6.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", @@ -1058,6 +1667,26 @@ "node": "^12.20.0 || ^14.13.1 || >=16.0.0" } }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, "node_modules/commander": { "version": "13.1.0", "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz", @@ -1067,6 +1696,13 @@ "node": ">=18" } }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, "node_modules/content-disposition": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", @@ -1155,7 +1791,6 @@ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "license": "MIT", - "peer": true, "dependencies": { "ms": "^2.1.3" }, @@ -1168,6 +1803,13 @@ } } }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, "node_modules/default-browser": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.0.tgz", @@ -1335,6 +1977,230 @@ "node": ">=8" } }, + "node_modules/eslint": { + "version": "9.39.5", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.5.tgz", + "integrity": "sha512-DgZS62aPLXKlnxILS/AYCoRvHaZeXceIzlXPkkGGzJWSow1aEk0lbTlxUSlyjC8jcaKxAdOnTDz+o1JFSBsyjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.2", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.6", + "@eslint/js": "9.39.5", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/eslint/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/eslint/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/eslint/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/etag": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", @@ -1434,6 +2300,20 @@ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "license": "MIT" }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, "node_modules/fast-uri": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", @@ -1482,6 +2362,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/finalhandler": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", @@ -1497,13 +2390,51 @@ "statuses": "^2.0.1" }, "engines": { - "node": ">= 18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" } }, + "node_modules/flatted": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.3.tgz", + "integrity": "sha512-/zipXxyO6rGvuNGDiULY9MvEGSkb2gaG4GGH4ygMi0ZZzyMHdUZBmntJmx5x1G2VuPytCwGN4xsJP6cw+sK+vQ==", + "dev": true, + "license": "ISC" + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -1599,6 +2530,32 @@ "node": ">= 0.4" } }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "17.7.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.7.0.tgz", + "integrity": "sha512-Czmyns5dUsq4seFBR/Kdydhmo8y9kC79hiSkPn0YcGtNnYWnrgt0vjrSjx9tspoDGWm2CMarffRuLjM4xUz8xg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -1612,6 +2569,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/has-symbols": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", @@ -1683,6 +2650,43 @@ "url": "https://opencollective.com/express" } }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, "node_modules/indent-string": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-5.0.0.tgz", @@ -1854,6 +2858,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-fullwidth-code-point": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", @@ -1869,6 +2883,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-in-ci": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-in-ci/-/is-in-ci-2.0.0.tgz", @@ -1951,6 +2978,36 @@ "url": "https://github.com/sponsors/panva" } }, + "node_modules/js-yaml": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", + "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, "node_modules/json-schema-traverse": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", @@ -1964,6 +3021,37 @@ "license": "BSD-2-Clause", "peer": true }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/lightningcss": { "version": "1.32.0", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", @@ -2213,6 +3301,29 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -2282,12 +3393,24 @@ "node": ">=6" } }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/nanoid": { "version": "3.3.12", @@ -2307,6 +3430,13 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, "node_modules/negotiator": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", @@ -2404,6 +3534,69 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -2423,6 +3616,16 @@ "node": "^12.20.0 || ^14.13.1 || >=16.0.0" } }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -2535,6 +3738,32 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.8.4", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.4.tgz", + "integrity": "sha512-N2MylSdi48+5N/6S5j+maeHbUSIzzZ5uOcX5Hm4QpV8Dkb1HFjfAKTKX6yNPJQD9AhcT3ifHNB66tWTTJDi11Q==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/process-warning": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz", @@ -2565,6 +3794,16 @@ "node": ">= 0.10" } }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/qs": { "version": "6.15.1", "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.1.tgz", @@ -2667,6 +3906,16 @@ "node": ">=0.10.0" } }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/restore-cursor": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-4.0.0.tgz", @@ -2766,6 +4015,19 @@ "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", "license": "MIT" }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/send": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", @@ -3017,6 +4279,19 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/stubborn-fs": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/stubborn-fs/-/stubborn-fs-2.0.0.tgz", @@ -3032,6 +4307,19 @@ "integrity": "sha512-zOh9jPYI+xrNOyisSelgym4tolKTJCQd5GBhK0+0xJvcYDcwlOoxF/rnFKQ2KRZknXSG9jWAp66fwP6AxN9STg==", "license": "MIT" }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/tagged-tag": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/tagged-tag/-/tagged-tag-1.0.0.tgz", @@ -3102,6 +4390,19 @@ "node": ">=0.6" } }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -3109,6 +4410,19 @@ "license": "0BSD", "optional": true }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/type-fest": { "version": "5.7.0", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.7.0.tgz", @@ -3139,6 +4453,44 @@ "node": ">= 0.6" } }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.65.0.tgz", + "integrity": "sha512-/ggrHAwyjENDusvyxbuqxAC2dTnZg/Z8F+fgQtYIz+L6n/9HfSlEZcFGV/NsMNa6CkGk0xUjUAFwC0vHOflvIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.65.0", + "@typescript-eslint/parser": "8.65.0", + "@typescript-eslint/typescript-estree": "8.65.0", + "@typescript-eslint/utils": "8.65.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, "node_modules/undici": { "version": "8.5.0", "resolved": "https://registry.npmjs.org/undici/-/undici-8.5.0.tgz", @@ -3157,6 +4509,16 @@ "node": ">= 0.8" } }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -3279,6 +4641,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/wrap-ansi": { "version": "9.0.2", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", @@ -3371,6 +4743,19 @@ "url": "https://github.com/sponsors/eemeli" } }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/yoga-layout": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/yoga-layout/-/yoga-layout-3.2.1.tgz", diff --git a/package.json b/package.json index 3b6e32493..26f97f0ae 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,12 @@ "build:launcher": "cd clients/launcher && npm run build", "ci": "npm run validate && npm run coverage && npm run smoke && npm run ci:storybook", "ci:storybook": "cd clients/web && npx playwright install chromium && npm run test:storybook", - "validate": "npm run validate:web && npm run validate:cli && npm run validate:tui && npm run validate:launcher", + "validate": "npm run validate:core && npm run validate:web && npm run validate:cli && npm run validate:tui && npm run validate:launcher", + "validate:core": "npm run format:check:core && npm run lint:core", + "lint:core": "eslint \"core/**/*.{ts,tsx}\"", + "format:core": "prettier --write \"core/**/*.{ts,tsx}\"", + "format:check:core": "prettier --check \"core/**/*.{ts,tsx}\"", + "format": "npm run format:core && cd clients/web && npm run format && cd ../cli && npm run format && cd ../tui && npm run format && cd ../launcher && npm run format", "validate:cli": "cd clients/cli && npm run validate", "validate:tui": "cd clients/tui && npm run validate", "validate:web": "cd clients/web && npm run validate", @@ -88,5 +93,13 @@ }, "engines": { "node": ">=22.19.0" + }, + "devDependencies": { + "@eslint/js": "^9.39.5", + "eslint": "^9.39.5", + "globals": "^17.7.0", + "prettier": "^3.8.1", + "typescript": "~5.9.3", + "typescript-eslint": "^8.65.0" } } diff --git a/pr-screenshots/README.md b/pr-screenshots/README.md deleted file mode 100644 index e4320389b..000000000 --- a/pr-screenshots/README.md +++ /dev/null @@ -1,318 +0,0 @@ -# Connection Info modal: sticky header while body scrolls (#1754) — proof screenshots - -Same fix as #1698, applied to the **Connection Info** modal. With a real server -whose `instructions` are long (e.g. the everything server in a typical config), -the connection details overflow the viewport; before the fix the whole modal -scrolled and the header left the view. Captured from the `ConnectionInfoModal` -Storybook story (`WithOAuth`, ~620px viewport) via Playwright. - -![Connection Info scrolled to the top — header pinned, Server Implementation visible](ci-1754-header-pinned-top.png) - -Top of scroll: the **Connection Info** header (title + close) sits above the -first section. - -![Connection Info scrolled down — same header still pinned while the body shows Server Instructions / OAuth Details](ci-1754-header-pinned-scrolled.png) - -Scrolled down (now showing Server Instructions → OAuth Details): the **same -header stays pinned**. Measured in-page: the body scroll container is -`scrollHeight` 1172 × `clientHeight` 558 (scrolls internally), header -`position: sticky` flush to the dialog top. - ---- - -# Server/Client Settings modal: sticky header while body scrolls (#1698) — proof screenshots - -Expanding enough accordion sections grew the Server Settings modal past the -viewport, scrolling the whole modal and taking the header (title + close) out of -view. The fix moves the header into a real `Modal.Header` (sticky by design) and -adds `scrollAreaComponent={ScrollArea.Autosize}` so only `Modal.Body` scrolls. -Captured from the `ServerSettingsModal` Storybook story (`FullyConfigured`, all -sections expanded, ~680px viewport) via Playwright. - -![Modal scrolled to the top — header pinned, Options section visible](ssm-1698-header-pinned-top.png) - -At the top of the scroll: the **Server Settings** header (collapse-all toggle, -title, close) sits above the first section. - -![Modal scrolled down — same header still pinned while the body shows Custom Headers / Request Metadata](ssm-1698-header-pinned-scrolled.png) - -Scrolled down through the body (now showing Advertised Extensions → Custom -Headers → Request Metadata): the **exact same header stays pinned** at the top. -Measured in-page: the body scroll container is `scrollHeight` 2253 × -`clientHeight` 612 (scrolls internally), and the header is `position: sticky` -with its top flush to the dialog top after scrolling. - ---- - -# TUI array-of-enum renders as a select on `items.enum` alone (#1751) — proof screenshots - -Follow-up to #1691. The TUI `schemaToForm` array-of-enum branch was nested under -`if (property.enum)`, so a standard array-of-enums schema — -`{ type: "array", items: { enum, enumNames } }` with **no** top-level `enum` — -fell through to a plain text input. The fix keys the array case on `items.enum` -alone (matching the web `SchemaForm`). Captured non-interactively (a standalone -render of the real `ink-form` built by the real `schemaToForm`). - -The demo schema has a `Sizes` array-of-enum field (`enum: [s,m,l]` + -`enumNames: [Small, Medium, Large]`, `default: "m"`, **no** top-level `enum`) -and a `Priority` single-enum control (has a top-level `enum`). - -![Before #1751 — the array-of-enum field is a plain text input showing the raw default "m"](tui-1751-before-string.png) - -**Before the fix** (reproducing the old nesting): `Sizes` is a plain **text -field** showing the raw default **`m`** — `enumNames` never applies because it -isn't a select at all. - -![With the #1751 fix — the array-of-enum field is a select resolving "m" to its title "Medium"](tui-1751-after-select.png) - -**With the fix:** `Sizes` is now a **select**; the default `m` resolves to its -title **`Medium`**. The `Priority` control (which has a top-level `enum`) shows -`High` in both — unchanged, confirming the reorder only affects the intended -`items.enum`-only case. - ---- - -# TUI enum forms honor `enumNames` (#1691) — proof screenshots - -The TUI tool-test form (`schemaToForm` → `ink-form`) rendered against a legacy -titled enum: `enum: [pet-1 … pet-5]` paired with the non-standard -`enumNames: [Cats … Reptiles]`, `default: "pet-1"`. Captured non-interactively -(a standalone render of the real `ink-form` built by the real `schemaToForm`; -the Ink TUI can't take live keystrokes in a headless pty). - -![TUI form with the #1691 fix — the required field resolves pet-1 to its title "Cats"](tui-enumnames-after-cats.png) - -**With the fix:** the required **Favorite Pet** field resolves the default -`pet-1` to its human title **"Cats"**. The submitted value stays the raw -`pet-1` — only the display label changes. (`Plain Enum`, which has no -`enumNames`, is unaffected.) - -![The same form before #1691 — the field shows the raw wire value "pet-1"](tui-enumnames-before-raw.png) - -**Before the fix** (same schema with `enumNames` stripped, reproducing the old -behavior): the field shows the opaque wire value **"pet-1"** with no indication -it means "Cats". - -Ground-truth options from the real `schemaToForm` confirm the label↔value -pairing and the length-guarded fallbacks: `pet` → `Cats=>pet-1 … Reptiles=>pet-5`; -array-of-enum `sizes` → `Small=>s | Medium=>m | Large=>l`; a mismatched-length -`enumNames` (2 names, 3 values) falls back to raw `a=>a | b=>b | c=>c`. The web -side of this fix is covered by `SchemaForm.test.tsx`. - ---- - -# Server Settings OAuth field disambiguation under EMA (#1692) — proof screenshots - -The **Server Settings → OAuth Settings** fields, rendered from the -`ServerSettingsForm` Storybook stories. Under enterprise-managed authorization -(EMA) these fields hold the *resource authorization server* credentials (leg 3 — -its registered client), **not** the app/IdP pair configured in Client Settings. -The unqualified labels sat directly under a toggle naming the enterprise IdP, -making it easy to paste the wrong pair. See #1692. - -![OAuth Settings with EMA off — plain Client ID / Client Secret](ema-oauth-fields-plain.png) - -Enterprise-managed authorization **off** (the common case): the fields keep the -plain **Client ID** / **Client Secret** labels. The **Scopes** field now -documents its delimiter — _"Space-separated OAuth scopes (RFC 6749). Do not use -commas — a comma-separated entry is sent as one invalid token and rejected by -the authorization server."_ — with a `mcp tools:read env:read` placeholder. - -![OAuth Settings with EMA on — Resource AS Client ID / Secret](ema-oauth-fields-resource-as.png) - -Enterprise-managed authorization **on**: the same two fields relabel to -**Resource AS Client ID** / **Resource AS Client Secret**, each gaining a -description that names the resource authorization server (its registered client, -EMA leg 3) and points the app client id/secret back to Client Settings. - -![Client Settings modal — IdP Client ID / IdP Client Secret](ema-client-settings-idp-fields.png) - -The reciprocal side in the **Client Settings** modal (Enterprise-Managed -Authorization section). Its credential fields are relabeled **IdP Client ID** / -**IdP Client Secret** — parallel to Server Settings' `Resource AS …` — and each -description now points the *other* way: these are the enterprise IdP pair (EMA -legs 1–2), **not** the per-server resource authorization server credentials, -which go in Server Settings → OAuth Settings. Together the two modals close the -loop: each names its pair and where the other lives. - ---- - -# Connection Info extensions + `io.modelcontextprotocol/ui` (#1740) — proof screenshots - -End-to-end verification of the Phase 3 Connection Info UI against the legacy -`advertised-extensions-http.json` test server (port 3220), driven in a real -browser through the web client's remote-proxy transport. - -![Connection Info on a legacy connection — Advertised Extensions shows tasks + ui](advertised-ext-conninfo-legacy.png) - -The Connection Info modal on a **legacy** connection (Era: **LEGACY**, no -Discovery section). The new **Server Extensions / Advertised Extensions** -two-column section renders — proving it is era-transparent (before #1740, -extensions only appeared in the modern-only Discovery section). **Advertised -Extensions** shows `io.modelcontextprotocol/tasks, io.modelcontextprotocol/ui`, -confirming the Inspector now advertises the MCP Apps `ui` extension. (Server -Extensions is `—` because this test server advertises none server-side.) - -![Connection Info after toggling Tasks off — Advertised Extensions shows only ui](advertised-ext-conninfo-tasksoff.png) - -After unchecking **Tasks** in Server Settings → Advertised Extensions (Phase 2) -and reconnecting, the **Advertised Extensions** column shows only -`io.modelcontextprotocol/ui` — the display reflects the per-server override live, -and the `ui` advertisement persists (it is a separate registry entry). - -> Note: the Connection Info modal is populated from the connection's -> `initializeResult`, which requires `serverInfo`. On the modern test server used -> here `serverInfo` wasn't populated, so the modal stays closed on that modern -> connection — a pre-existing connection-layer behavior, independent of this PR -> (which only changes content *inside* the modal). The era-transparent -> **Server Extensions** path is covered by unit tests -> (`ConnectionInfoContent.test.tsx` renders legacy server extensions with no -> `discoverResult`). - ---- - -# Advertised-extensions toggle (#1739) — proof screenshots - -End-to-end verification of the advertised-extensions debugging knob against a -real legacy HTTP test server (`test-servers/configs/advertised-extensions-http.json` -on port 3220), driven in a real browser through the web client's remote-proxy -transport. The server registers `echo` (always) and `get_weather` **gated on the -`io.modelcontextprotocol/tasks` extension** (`extensionGatedTools`): the tool is -enabled on `notifications/initialized` only when the connected client declared -that extension. - -![Advertised Extensions — Tasks advertised (default)](advertised-ext-settings-tasks-on.png) - -**Server Settings → Advertised Extensions** (the new section). With no override, -**Tasks (io.modelcontextprotocol/tasks)** is checked — the registry default — -so the Inspector advertises it in its client capabilities. - -![Tools with Tasks advertised](advertised-ext-tools-tasks-on.png) - -Connected with Tasks advertised. The server sees the declared extension on -`initialized` and enables the gated tool, so `tools/list` returns **both `echo` -and `get_weather`**. - -![Advertised Extensions — Tasks unchecked](advertised-ext-settings-tasks-off.png) - -Unchecking **Tasks** writes the per-server `advertisedExtensions` override. The -change takes effect on the next connect (as the section note says). - -![Tools with Tasks not advertised](advertised-ext-tools-tasks-off.png) - -After reconnecting, the client advertises no extensions, so the server never -enables the gated tool — `tools/list` now returns **only `echo`**. The server's -tool registration demonstrably changed based on what the client advertised, -exactly the acceptance criterion for #1739. - ---- - -# x-mcp-header Tools tooling (#1632) — proof screenshots - -End-to-end verification of the SEP-2243 `x-mcp-header` Tools tooling against a -real modern (2026-07-28) HTTP test server -(`test-servers/configs/xmcpheader-modern-http.json` on port 3120), driven in a -real browser through the web client's remote-proxy transport. - -![Excluded tools in the sidebar](xmcpheader-excluded-tools.png) - -Connected with **Protocol Era = Modern**. `invalid_header_tool` is dropped from -`tools/list` by the SDK (its `x-mcp-header` annotation is invalid), so the -Inspector re-lists the raw list and surfaces it struck-through under an -**"Excluded (SEP-2243)"** divider — showing _why_ a tool vanished rather than -silently omitting it. - -![Exclusion reason on hover](xmcpheader-excluded-reason.png) - -Hovering the excluded tool shows the exact scan reason: the header name -`"Bad Header"` contains a space, so it is not a valid RFC 9110 token. - -![Mirrored request headers](xmcpheader-mirrored-headers.png) - -`get_weather`'s detail panel shows the **"Mirrored request headers (SEP-2243)"** -section: its `city` argument mirrors to `Mcp-Param-City`, with the note that the -SDK omits `Mcp-Param-*` on the browser transport. - -![Unknown tool -32602](xmcpheader-unknown-tool.png) - -Calling a tool the server no longer recognizes rejects with **`-32602`** (SDK v2) -instead of an `isError` result, and renders as an **"Unknown Tool"** error panel -with a targeted hint (reproduced by swapping the sessionless server to one -without `echo` while the cached list still showed it). - -![Invalid params -32602](xmcpheader-invalid-params.png) - -`-32602` is the generic _Invalid params_ code, so a **known** tool rejected for -bad arguments throws the same code as an unknown tool. The panel disambiguates -from the message: a `-32602` that does not name an unknown tool renders under -**"Invalid Parameters"** (with a schema hint) rather than "Unknown Tool". -Triggered live via the `trigger_invalid_params` tool, which returns a real -`-32602` JSON-RPC error whose message is not about a missing tool. - ---- - -# Tasks extension era fork (#1631) — proof screenshots - -End-to-end verification of the Tasks era fork against two real test servers -(`test-servers/configs/tasks-modern-http.json` on port 3222 and -`tasks-legacy-http.json` on 3223), driven in a real browser through the web -client's remote-proxy transport. - -## Modern era (2026-07-28, `io.modelcontextprotocol/tasks` extension) - -![Modern connected, Tasks tab present](tasks-modern-connected.png) - -Connected with **Protocol Era = Modern** (`MCP 2026-07-28`). The **Tasks** tab -appears in the monitoring sidebar because the `io.modelcontextprotocol/tasks` -extension was negotiated (it is empty until a task runs). - -![Modern task completed](tasks-modern-completed.png) - -Connected with **Protocol Era = Modern** (`MCP 2026-07-28`). The **Tasks** tab is -gated on the negotiated `io.modelcontextprotocol/tasks` extension (not -`capabilities.tasks`). Running `modern_task` with **Run as task** on issues a -`tools/call` that returns a `CreateTaskResult` (`resultType: "task"`), then polls -**`tasks/get`** (no `tasks/list`); the completed task **inlines its result** (no -blocking `tasks/result`) — shown both in the Results panel and the Tasks card. - -![Modern input_required → tasks/update](tasks-modern-input-required.png) - -`modern_input_task` moves to **`input_required`**: the `tasks/get` response's -`inputRequests` map (visible in the task's Full Task Object) carries an embedded -`elicitation/create`, surfaced through the same pending-request modal the MRTR -path uses — note the accurate wording _"your answer is submitted via a -tasks/update request (SEP-2663), not a retry"_. Answering it sends -**`tasks/update`** with the `inputResponses`, and the next poll completes the -task: - -![Modern input task completed](tasks-modern-input-completed.png) - -## Legacy era (2025-11-25, contrast — unchanged) - -![Legacy run-as-task](tasks-legacy-run-as-task.png) - -Connected with **Protocol Era = Legacy**. The Tasks tab is gated on -`capabilities.tasks`; `simple_task` is `taskSupport: "required"` so **Run as -task** is forced on. The legacy flow uses `tasks/list` to populate the list, -`tasks/get` to poll, and the blocking **`tasks/result`** to fetch the payload -(`{ "message": "Task completed: no message", "taskId": … }`). Note the legacy-only -**Logs** tab (this server advertises `logging`), absent from the modern monitor -set. - -## Notes - -- SDK v2 removed all tasks support **and** era-gates the `tasks/*` spec methods - out of the 2026-07-28 era on **both** the client (outbound) and server - (inbound), and its codec rejects a `resultType: "task"` result outright. So the - Inspector drives the extension itself: the task-creation frame is rewritten at - the transport into a `CallToolResult` carrying the handle (the true - `resultType: "task"` frame is still logged to the Protocol/Network tabs), and - `tasks/get` / `tasks/update` / `tasks/cancel` ride a raw-wire request channel - that carries the full modern envelope and is consumed by the transport before - the SDK Client sees it. The test server serves `tasks/*` from an Express - interceptor ahead of the SDK handler (the SDK's modern leg would answer them - `-32601`). -- On modern, task creation is **server-directed** (SEP-2663): the client declares - the extension once and any tool may return a task, so the Tools screen offers - **Run as task** for every tool on a modern connection (rather than gating on the - legacy per-tool `taskSupport`). diff --git a/pr-screenshots/adv-ext-own-section-conninfo.png b/pr-screenshots/adv-ext-own-section-conninfo.png deleted file mode 100644 index 3e9bccc19..000000000 Binary files a/pr-screenshots/adv-ext-own-section-conninfo.png and /dev/null differ diff --git a/pr-screenshots/adv-ext-own-section.png b/pr-screenshots/adv-ext-own-section.png deleted file mode 100644 index c42945f18..000000000 Binary files a/pr-screenshots/adv-ext-own-section.png and /dev/null differ diff --git a/pr-screenshots/advertised-ext-conninfo-legacy.png b/pr-screenshots/advertised-ext-conninfo-legacy.png deleted file mode 100644 index c9957c7b3..000000000 Binary files a/pr-screenshots/advertised-ext-conninfo-legacy.png and /dev/null differ diff --git a/pr-screenshots/advertised-ext-conninfo-tasksoff.png b/pr-screenshots/advertised-ext-conninfo-tasksoff.png deleted file mode 100644 index 69f0fcef5..000000000 Binary files a/pr-screenshots/advertised-ext-conninfo-tasksoff.png and /dev/null differ diff --git a/pr-screenshots/advertised-ext-settings-tasks-off.png b/pr-screenshots/advertised-ext-settings-tasks-off.png deleted file mode 100644 index 06af4e668..000000000 Binary files a/pr-screenshots/advertised-ext-settings-tasks-off.png and /dev/null differ diff --git a/pr-screenshots/advertised-ext-settings-tasks-on.png b/pr-screenshots/advertised-ext-settings-tasks-on.png deleted file mode 100644 index 6fd72f3b3..000000000 Binary files a/pr-screenshots/advertised-ext-settings-tasks-on.png and /dev/null differ diff --git a/pr-screenshots/advertised-ext-tools-tasks-off.png b/pr-screenshots/advertised-ext-tools-tasks-off.png deleted file mode 100644 index 482196465..000000000 Binary files a/pr-screenshots/advertised-ext-tools-tasks-off.png and /dev/null differ diff --git a/pr-screenshots/advertised-ext-tools-tasks-on.png b/pr-screenshots/advertised-ext-tools-tasks-on.png deleted file mode 100644 index 4d536b65e..000000000 Binary files a/pr-screenshots/advertised-ext-tools-tasks-on.png and /dev/null differ diff --git a/pr-screenshots/ci-1754-header-pinned-scrolled.png b/pr-screenshots/ci-1754-header-pinned-scrolled.png deleted file mode 100644 index 17dea3c26..000000000 Binary files a/pr-screenshots/ci-1754-header-pinned-scrolled.png and /dev/null differ diff --git a/pr-screenshots/ci-1754-header-pinned-top.png b/pr-screenshots/ci-1754-header-pinned-top.png deleted file mode 100644 index 15a23f857..000000000 Binary files a/pr-screenshots/ci-1754-header-pinned-top.png and /dev/null differ diff --git a/pr-screenshots/ema-client-settings-idp-fields.png b/pr-screenshots/ema-client-settings-idp-fields.png deleted file mode 100644 index 7a2d013fc..000000000 Binary files a/pr-screenshots/ema-client-settings-idp-fields.png and /dev/null differ diff --git a/pr-screenshots/ema-oauth-fields-plain.png b/pr-screenshots/ema-oauth-fields-plain.png deleted file mode 100644 index 44f733e29..000000000 Binary files a/pr-screenshots/ema-oauth-fields-plain.png and /dev/null differ diff --git a/pr-screenshots/ema-oauth-fields-resource-as.png b/pr-screenshots/ema-oauth-fields-resource-as.png deleted file mode 100644 index 267dafc59..000000000 Binary files a/pr-screenshots/ema-oauth-fields-resource-as.png and /dev/null differ diff --git a/pr-screenshots/logging-legacy-era.png b/pr-screenshots/logging-legacy-era.png deleted file mode 100644 index 65b890738..000000000 Binary files a/pr-screenshots/logging-legacy-era.png and /dev/null differ diff --git a/pr-screenshots/logging-modern-era-control.png b/pr-screenshots/logging-modern-era-control.png deleted file mode 100644 index 7f5f62704..000000000 Binary files a/pr-screenshots/logging-modern-era-control.png and /dev/null differ diff --git a/pr-screenshots/logging-modern-era-loglevel-stamp.png b/pr-screenshots/logging-modern-era-loglevel-stamp.png deleted file mode 100644 index ef6adab2c..000000000 Binary files a/pr-screenshots/logging-modern-era-loglevel-stamp.png and /dev/null differ diff --git a/pr-screenshots/logging-modern-server-setting.png b/pr-screenshots/logging-modern-server-setting.png deleted file mode 100644 index fb48084e8..000000000 Binary files a/pr-screenshots/logging-modern-server-setting.png and /dev/null differ diff --git a/pr-screenshots/ssm-1698-header-pinned-scrolled.png b/pr-screenshots/ssm-1698-header-pinned-scrolled.png deleted file mode 100644 index 327307d5b..000000000 Binary files a/pr-screenshots/ssm-1698-header-pinned-scrolled.png and /dev/null differ diff --git a/pr-screenshots/ssm-1698-header-pinned-top.png b/pr-screenshots/ssm-1698-header-pinned-top.png deleted file mode 100644 index 216b3a2e8..000000000 Binary files a/pr-screenshots/ssm-1698-header-pinned-top.png and /dev/null differ diff --git a/pr-screenshots/subscriptions-legacy-live-update.png b/pr-screenshots/subscriptions-legacy-live-update.png deleted file mode 100644 index 83e31faf1..000000000 Binary files a/pr-screenshots/subscriptions-legacy-live-update.png and /dev/null differ diff --git a/pr-screenshots/subscriptions-legacy-resources-subscribe.png b/pr-screenshots/subscriptions-legacy-resources-subscribe.png deleted file mode 100644 index e687a5cef..000000000 Binary files a/pr-screenshots/subscriptions-legacy-resources-subscribe.png and /dev/null differ diff --git a/pr-screenshots/subscriptions-modern-header-badge.png b/pr-screenshots/subscriptions-modern-header-badge.png deleted file mode 100644 index 737a6b5c4..000000000 Binary files a/pr-screenshots/subscriptions-modern-header-badge.png and /dev/null differ diff --git a/pr-screenshots/subscriptions-modern-listen-acknowledged.png b/pr-screenshots/subscriptions-modern-listen-acknowledged.png deleted file mode 100644 index 3bffe69d7..000000000 Binary files a/pr-screenshots/subscriptions-modern-listen-acknowledged.png and /dev/null differ diff --git a/pr-screenshots/tasks-legacy-cancel-sticky.png b/pr-screenshots/tasks-legacy-cancel-sticky.png deleted file mode 100644 index be3428a6a..000000000 Binary files a/pr-screenshots/tasks-legacy-cancel-sticky.png and /dev/null differ diff --git a/pr-screenshots/tasks-legacy-run-as-task.png b/pr-screenshots/tasks-legacy-run-as-task.png deleted file mode 100644 index ef88e3435..000000000 Binary files a/pr-screenshots/tasks-legacy-run-as-task.png and /dev/null differ diff --git a/pr-screenshots/tasks-modern-completed.png b/pr-screenshots/tasks-modern-completed.png deleted file mode 100644 index 7794b3c44..000000000 Binary files a/pr-screenshots/tasks-modern-completed.png and /dev/null differ diff --git a/pr-screenshots/tasks-modern-connected.png b/pr-screenshots/tasks-modern-connected.png deleted file mode 100644 index 50696436c..000000000 Binary files a/pr-screenshots/tasks-modern-connected.png and /dev/null differ diff --git a/pr-screenshots/tasks-modern-input-completed.png b/pr-screenshots/tasks-modern-input-completed.png deleted file mode 100644 index f7576d3d4..000000000 Binary files a/pr-screenshots/tasks-modern-input-completed.png and /dev/null differ diff --git a/pr-screenshots/tasks-modern-input-required.png b/pr-screenshots/tasks-modern-input-required.png deleted file mode 100644 index 4a05bfc07..000000000 Binary files a/pr-screenshots/tasks-modern-input-required.png and /dev/null differ diff --git a/pr-screenshots/tasks-modern-loop-cancelled.png b/pr-screenshots/tasks-modern-loop-cancelled.png deleted file mode 100644 index 75eaa5da2..000000000 Binary files a/pr-screenshots/tasks-modern-loop-cancelled.png and /dev/null differ diff --git a/pr-screenshots/tui-1751-after-select.png b/pr-screenshots/tui-1751-after-select.png deleted file mode 100644 index 70b6f843e..000000000 Binary files a/pr-screenshots/tui-1751-after-select.png and /dev/null differ diff --git a/pr-screenshots/tui-1751-before-string.png b/pr-screenshots/tui-1751-before-string.png deleted file mode 100644 index 477bff34c..000000000 Binary files a/pr-screenshots/tui-1751-before-string.png and /dev/null differ diff --git a/pr-screenshots/tui-enumnames-after-cats.png b/pr-screenshots/tui-enumnames-after-cats.png deleted file mode 100644 index 37d970bec..000000000 Binary files a/pr-screenshots/tui-enumnames-after-cats.png and /dev/null differ diff --git a/pr-screenshots/tui-enumnames-before-raw.png b/pr-screenshots/tui-enumnames-before-raw.png deleted file mode 100644 index 7cb6c55fc..000000000 Binary files a/pr-screenshots/tui-enumnames-before-raw.png and /dev/null differ diff --git a/pr-screenshots/xmcpheader-excluded-reason.png b/pr-screenshots/xmcpheader-excluded-reason.png deleted file mode 100644 index f8a0c6308..000000000 Binary files a/pr-screenshots/xmcpheader-excluded-reason.png and /dev/null differ diff --git a/pr-screenshots/xmcpheader-excluded-tools.png b/pr-screenshots/xmcpheader-excluded-tools.png deleted file mode 100644 index 31c826ef9..000000000 Binary files a/pr-screenshots/xmcpheader-excluded-tools.png and /dev/null differ diff --git a/pr-screenshots/xmcpheader-invalid-params.png b/pr-screenshots/xmcpheader-invalid-params.png deleted file mode 100644 index ee4d7f8c6..000000000 Binary files a/pr-screenshots/xmcpheader-invalid-params.png and /dev/null differ diff --git a/pr-screenshots/xmcpheader-mirrored-headers.png b/pr-screenshots/xmcpheader-mirrored-headers.png deleted file mode 100644 index 34399e1d6..000000000 Binary files a/pr-screenshots/xmcpheader-mirrored-headers.png and /dev/null differ diff --git a/pr-screenshots/xmcpheader-unknown-tool.png b/pr-screenshots/xmcpheader-unknown-tool.png deleted file mode 100644 index 8e8e14062..000000000 Binary files a/pr-screenshots/xmcpheader-unknown-tool.png and /dev/null differ