Merge pull request #1903 from modelcontextprotocol/v2/chore/milestone-merge-v2.1.0
Some checks failed
Run install, format, lint, build, and test on every push / build (push) Has been cancelled
Run install, format, lint, build, and test on every push / publish (push) Has been cancelled
Run install, format, lint, build, and test on every push / publish-github-container-registry (push) Has been cancelled

chore: merge v2/main for the v2.1.0 milestone release
This commit is contained in:
Cliff Hall 2026-08-05 11:47:35 -04:00 • committed by GitHub
commit c7bccd477d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
60 changed files with 10995 additions and 8298 deletions

1
.gitattributes vendored Normal file
View file

@ -0,0 +1 @@
package-lock.json linguist-generated=true

163
.github/ISSUE_TEMPLATE/1-bug_report.yml vendored Normal file
View file

@ -0,0 +1,163 @@
name: Bug report
description: Report something broken in the Inspector (web, CLI, or TUI)
labels: ["bug"]
body:
- type: markdown
attributes:
value: |
Thanks for reporting a bug — **issues are how work reaches the Inspector.**
This repository accepts **issues, not pull requests**: design and
implementation are done by the maintainers through a prompt-driven
workflow ([`CONTRIBUTING.md`](https://github.com/modelcontextprotocol/inspector/blob/main/CONTRIBUTING.md)).
So if you have already prototyped a fix locally, **the prompt you used
plus screenshots is worth more to us than a diff** — there is a field
for it at the bottom of this form.
> 🔒 **Do not report security vulnerabilities here.** Use the
> [private advisory process](https://github.com/modelcontextprotocol/inspector/security/advisories/new)
> instead — see [`SECURITY.md`](https://github.com/modelcontextprotocol/inspector/blob/main/SECURITY.md).
- type: dropdown
id: version-line
attributes:
label: Which version line?
description: >
v2 is the current Inspector. v1 is deprecated and takes **security
fixes only**, so a v1 bug report gets a different answer. When in
doubt, it's v2.
options:
- v2 — current (`@modelcontextprotocol/inspector@latest`)
- v1 — deprecated (`@v1-latest`, security fixes only)
validations:
required: true
- type: dropdown
id: client
attributes:
label: Which client?
description: The Inspector ships three clients from one package.
options:
- Web
- CLI
- TUI
- All / shared core
validations:
required: true
- type: input
id: inspector-version
attributes:
label: Inspector version
description: The version you actually ran, not "latest".
placeholder: "2.0.0"
validations:
required: true
- type: input
id: node-version
attributes:
label: Node version
description: >
Output of `node --version`. The current v2 line requires Node >=
22.19.0; the deprecated v1 line has its own, older floor.
placeholder: "v22.19.0"
validations:
required: true
- type: input
id: os
attributes:
label: Operating system (and browser, for the web client)
placeholder: "macOS 15.5, Chrome 140"
validations:
required: false
- type: dropdown
id: transport
attributes:
label: Transport
description: How the Inspector was connected to the MCP server under inspection.
options:
- stdio
- Streamable HTTP
- SSE (legacy)
- Not applicable / never connected
validations:
required: true
- type: textarea
id: mcp-server
attributes:
label: MCP server under inspection
description: >
Which server, and how it is configured — the command or URL, the
protocol era you connected with (auto / legacy / modern), and whether
OAuth is involved. Redact tokens and secrets.
placeholder: |
Server: @modelcontextprotocol/server-everything (npx)
Protocol era: modern
OAuth: no
validations:
required: true
- type: textarea
id: repro
attributes:
label: Steps to reproduce
description: Numbered steps, starting from a fresh launch of the Inspector.
placeholder: |
1. Run `npx @modelcontextprotocol/inspector`
2. Add the server above and connect
3. Open the Tools tab and call `echo`
validations:
required: true
- type: textarea
id: expected
attributes:
label: Expected behavior
description: What you expected to happen.
validations:
required: true
- type: textarea
id: actual
attributes:
label: Actual behavior
description: What happened instead.
validations:
required: true
- type: textarea
id: logs
attributes:
label: Logs, errors, or screenshots
description: >
Console output, the Inspector's Protocol or Network tab, or a
screenshot. Redact tokens and secrets first. Screenshots can be pasted
directly into this field.
validations:
required: false
- type: textarea
id: prompt
attributes:
label: Already prototyped a fix?
description: >
Please don't attach a diff or open a pull request — share the **exact
prompt(s)** you used to produce the change, plus what you verified and
any before/after screenshots. We reproduce it through our own workflow
so it lands with the right conventions, tests, and coverage.
validations:
required: false
- type: checkboxes
id: acknowledgements
attributes:
label: Before you submit
options:
- label: I searched existing issues and this is not a duplicate.
required: true
- label: This is not a security vulnerability report (those go through the private advisory process).
required: true

View file

@ -0,0 +1,88 @@
name: Feature request
description: Suggest an improvement to the Inspector
# `v2` is safe to declare statically here: v1 takes security fixes only and
# cannot receive a feature, so a feature request is v2 by construction. The bug
# form can't do this — its version line is a reporter's answer, and `labels:`
# is static.
labels: ["enhancement", "v2"]
body:
- type: markdown
attributes:
value: |
Thanks for suggesting an improvement.
**Maintainers do the implementation here** — this repository accepts
**issues, not pull requests**
([`CONTRIBUTING.md`](https://github.com/modelcontextprotocol/inspector/blob/main/CONTRIBUTING.md)).
That makes a sharply-stated **problem** the most valuable thing you can
give us: it is what we design against, and it survives after a specific
solution turns out not to fit.
So please describe the problem first, and treat the solution section as
one possible answer rather than the request itself.
> **This form targets v2.** The v1 line is deprecated and takes
> **security fixes only** — it does not take new features.
- type: dropdown
id: client
attributes:
label: Which client?
options:
- Web
- CLI
- TUI
- All / shared core
- Not sure
validations:
required: true
- type: textarea
id: problem
attributes:
label: The problem
description: >
What are you trying to do, and what makes it hard or impossible today?
Describe the situation, not the feature — including how often you hit
it and what you do instead right now.
validations:
required: true
- type: textarea
id: solution
attributes:
label: Solution you have in mind (optional)
description: >
If you have a concrete idea, describe it here. It's fine to leave this
blank — the problem above is the part we need.
validations:
required: false
- type: textarea
id: alternatives
attributes:
label: Alternatives or workarounds you have tried
validations:
required: false
- type: textarea
id: prompt
attributes:
label: Already built it locally?
description: >
Please don't attach a diff or open a pull request — share the **exact
prompt(s)** you used, plus screenshots of the result. We reproduce the
work through our own workflow so it lands with the right conventions,
tests, and coverage.
validations:
required: false
- type: checkboxes
id: acknowledgements
attributes:
label: Before you submit
options:
- label: I searched existing issues and this is not a duplicate.
required: true
- label: This is a request for the Inspector itself, not for the MCP specification or an SDK.
required: true

57
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View file

@ -0,0 +1,57 @@
# Issue chooser configuration.
#
# Blank issues are disabled so every report lands in a form. The bug form
# requires the facts triage needs first (client, version line, transport); the
# feature form requires the client and a problem statement, and is v2-only.
#
# Security reports are deliberately a *contact link* rather than a template:
# a template would still open a public issue, which is exactly what a
# vulnerability report must not do. These links leave GitHub's issue flow and
# go straight to the private advisory form. See SECURITY.md.
blank_issues_enabled: false
contact_links:
- name: 🔒 Security vulnerability (v1 or v2) — report privately
url: https://github.com/modelcontextprotocol/inspector/security/advisories/new
about: >
Never report a vulnerability in a public issue. Private vulnerability
reporting is enabled on this repository — this link opens the advisory
form, which is the fastest route to a maintainer. v1 is deprecated and
takes security fixes only, so this is the channel for a v1 security
issue.
- name: 📜 Security policy (supported versions, what to include)
url: https://github.com/modelcontextprotocol/inspector/blob/main/SECURITY.md
about: >
Which versions are supported, how reports are handled, and what to
include in one.
- name: 🤝 Contribution policy — why there is no "New pull request"
url: https://github.com/modelcontextprotocol/inspector/blob/main/CONTRIBUTING.md
about: >
This repository accepts issues, not pull requests — maintainers do the
implementation. If you already built a change locally, open an issue and
share the prompt you used rather than a diff.
- name: 📖 MCP documentation
url: https://modelcontextprotocol.io
about: >
Guides and reference for the Model Context Protocol itself. Start here
for "how does MCP work" questions.
- name: 📐 MCP specification — protocol questions and proposals
url: https://github.com/modelcontextprotocol/modelcontextprotocol/issues
about: >
If the behavior you're reporting is defined by the protocol rather than
by this tool, file it against the specification repository instead.
- name: 🧰 MCP TypeScript SDK
url: https://github.com/modelcontextprotocol/typescript-sdk/issues
about: >
The Inspector is built on the TypeScript SDK. If the bug reproduces
against the SDK directly (outside the Inspector), file it there.
- name: 💬 MCP Contributor Discord (#inspector-dev)
url: https://discord.gg/6CSzBmMkjX
about: >
Questions, usage help, and day-to-day Inspector development discussion.
Also where to say hello if you'd like to work on the Inspector with us.

105
.github/copilot-instructions.md vendored Normal file
View file

@ -0,0 +1,105 @@
# Copilot review instructions — MCP Inspector
> **`AGENTS.md` is the source of truth.** This file is a review-focused distillation of it, kept in sync by hand — see "Keep documentation files up to date" in `AGENTS.md`. Where the two disagree, `AGENTS.md` wins, and the drift is a bug worth flagging in review.
The Inspector ships as one package with three clients (**Web**, **CLI**, **TUI**) over a shared `core/`, consumed via the `@inspector/core` build-time alias. v2 is **not** an npm workspace: the root and each `clients/*` keep their own `package.json` and `node_modules`.
## TypeScript
- **Never use `any`.** Not in types, not in casts, not in generics.
- **Never suppress errors to satisfy the linter or compiler** — no disabling `no-unused-vars` / `no-explicit-any` in config, and no `// @ts-nocheck` or `// @ts-ignore` (`@typescript-eslint/ban-ts-comment` rejects these across every surface).
- **Avoid double casts (`as unknown as T`).** They erase all type safety and usually mean the real type is being worked around. Prefer a type guard, a narrower single cast, or fixing the underlying type. If genuinely unavoidable (a documented gap in a third-party type, or bridging structurally-identical shapes TS can't relate), it **must** carry an inline comment justifying why it's safe and why nothing better exists. An unjustified `as unknown as` is not acceptable in review.
- Prefer inference, type guards, and precise annotations over assertions.
- An `_` prefix is the intentionally-unused marker (`argsIgnorePattern` / `varsIgnorePattern` / `caughtErrorsIgnorePattern`).
## React and UI (web client)
The web client is built from **presentational ("dumb") components** — they take data and callbacks as props and hold display logic only. No data fetching or client state inside them; state comes from the `@inspector/core` hooks wired near the top of the tree. A component that reaches for a store or fetches directly is a review finding.
Styling is **Mantine-first**, in this strict order of preference: component props → theme variants → CSS classes (last resort).
- **Never use inline styles.**
- **Never use raw color literals** — no hex (`#ddd`), no `rgba()`. Use the `--inspector-*` CSS custom properties from `App.css :root` (e.g. `c: 'var(--inspector-text-primary)'`). If no token fits, add one to `:root` first.
- **Avoid `div` and bare HTML for layout.** Use Mantine `Box`, `Group`, `Stack`, `Flex`, `Paper`.
- **Never add a CSS class when the styles can be component props or a theme variant.** Flat CSS properties (margin, padding, background, border, color, font-size) belong in the theme (`src/theme/<Component>.ts`, via `Component.extend()`). `App.css` may contain **only** what the theme cannot express: `@keyframes`, pseudo-selectors (`:hover`, `:focus`), cross-component hover relationships, nested child selectors for third-party HTML output, and styles for native elements (`img`, `iframe`).
- When a theme variant needs a class for nested/pseudo selectors, assign it via `classNames` in the theme extension — never a manual `className` in JSX for theme-styled components.
### The `.withProps()` rule
**Declare a named subcomponent constant via `.withProps()` whenever an inline Mantine element carries two or more _static_ props.** This applies to single-use elements too — "it's only used once" is not an exemption.
- **Static** = a literal configuring **styling, layout, or behavior**: `size="sm"`, `c="dimmed"`, `fw={500}`, `gap="xs"`, `justify="space-between"`, `variant="light"`, `withBorder`, `readOnly`, `striped`.
- **Not counted:** dynamic props (`value`, `on*`, `children`, `key`, `ref`, anything whose value is a variable) — pass these at the call site; and per-instance **content/accessibility** literals (`label`, `description`, `placeholder`, `title`, `aria-label`, `role`) — these never by themselves trigger extraction.
```tsx
const CardContent = Group.withProps({
flex: 1,
align: "flex-start",
justify: "space-between",
wrap: "nowrap",
});
```
**Legitimate exceptions** (each stays inline, with a one-line comment saying why):
- **`Box`** — does not support `.withProps()`. Use `Group`/`Stack`/`Flex`/`Text`/`Paper`/`UnstyledButton`/`Image` instead, chosen by purpose. A `Box` that genuinely needs a non-flex primitive (`component="iframe"`, `display="grid"`) stays inline.
- **`Accordion`** — a compound, `multiple`-discriminated generic; `.withProps()` loses its JSX call signature and fails to type.
- **Headless, non-`factory()` components** such as `Transition` — no Styles API, so no `.withProps` static at all.
- **`data-*` attributes** — not part of a component's typed props, so excess-property-checked out of a `withProps` literal. Pass at the call site.
- **Anything that isn't a Mantine factory component** — a `react-icons` glyph, another library's component, or a first-party plain `export function`.
### State and effects
- **Never reset or re-sync local state from a prop inside a `useEffect`.** `useEffect(() => setX(prop), [prop])` paints the stale value first and renders twice; it is an error under `react-hooks/set-state-in-effect`.
- Use **`useValueChange(value, onChange)`** (`src/hooks/useValueChange.ts`) — React's documented "adjusting state during render" pattern. It does not fire on the first render; seed the state with `useState`. The comparison is `Object.is`, so pass a **referentially stable** value — a primitive key (id/name/URI) or a memoized one, never a fresh object literal.
- The `onChange` runs **during render**, so it must be pure — `setState` and nothing else. No fetches, DOM writes, logging, ref mutation, or parent callbacks; a render can be replayed or abandoned.
- Effects remain correct for real external-system synchronization (DOM measurement, rAF, subscriptions, timers).
### Theme files vs. element components
Both exist and do different jobs. Theme files (`src/theme/<Component>.ts`) customize a Mantine primitive **app-wide**. Element components (`src/components/elements/`) add **domain semantics** on top of primitives.
- Element components import from `@mantine/core`, **not** from `src/theme/` — the theme layer is applied transparently by the provider.
- **Never push domain-specific variant logic into theme files** (annotation types, transport types, …). Domain variants belong to the element component that owns those semantics.
## Where code goes (web client)
**`utils` = functions that compute; `lib` = things that instantiate, adapt, or touch the environment.** If it does I/O or wraps a subsystem it's `lib`; if it's a pure transform it's `utils`.
- `src/utils/` — pure, side-effect-free. Also: pure shared domain types and their constructors; diagnostic `console.warn`/`error` does **not** count as a side effect; type-only imports from `@inspector/core`, and re-exporting pure functions/constants from core, are both fine.
- `src/lib/` — infrastructure, integration, stateful adapters: composes subsystems, wraps the core **runtime**, touches DOM / `window` / `sessionStorage`, or produces side effects.
- Cross-directory imports go **one way: `lib → utils`**, never the reverse.
- `src/types/` is only for ambient `.d.ts` module stubs — not a home for new domain types.
- ⚠️ The coverage `include` is a **whitelist** naming `components` / `hooks` / `theme` / `lib` / `utils` / `server` (plus the `core/*` runtime). A module placed outside those directories silently falls out of the ≥90% gate. Flag new top-level files or new grab-bag directories.
## Tests and the coverage gate
- **All new or modified code needs tests.** The per-file gate is **≥ 90% on all four dimensions** — lines, statements, functions, **and branches** — enforced in CI for `clients/{web,cli,tui,launcher}` and the gated `core/` runtime.
- **Never lower the gate** to accommodate an unreachable branch. Annotate at the source with a justified `/* v8 ignore … -- <reason> */`. Acceptable reasons: happy-dom-inherent paths (Mantine portal mounts, `useMediaQuery` fallbacks, `typeof window` SSR guards), React StrictMode effect-replay blocks, and provably-dead defensive guards. Anything else is a missing test.
- **Suppress expected error output** from the console in tests that exercise error paths.
### Test placement
- **Web:** side-by-side by default — `<Name>.test.tsx` next to the source. A web-owned test under `src/test/` instead of beside its source is a bug. `src/test/` is only for what can't be co-located: tests of the repo-root `core/` package (`src/test/core/…`, mirroring core's layout), the `integration` project (`src/test/integration/…` — placement _is_ the manifest), and shared test infrastructure.
- **CLI / TUI / launcher:** **all** tests live in a top-level `__tests__/` directory. A co-located `src/**/*.test.*` lands in no tsconfig project and fails `verify:typecheck-coverage`.
### Rendering components in tests
- **Always render through `renderWithMantine`** from `src/test/renderWithMantine.tsx`. Never hand-roll a bare `MantineProvider` — that reintroduces a real failure class where a `Transition`/`Modal` timer fires after happy-dom tears down `window` and fails the entire run.
- For a forced color scheme, pass the option — `renderWithMantine(ui, { colorScheme: "dark" })` — rather than a hand-rolled `defaultColorScheme` provider.
- Only when asserting _mid-flight_ transition state, use `renderWithMantineTransitions`, passing `settleMs` derived from the component's real animation duration. Do **not** combine it with `vi.useFakeTimers()`, and use the `unmount()` it returns if the test unmounts the tree itself.
## Gates and PR hygiene
- `npm run format` before committing; **`npm run ci` before pushing** (`validate` → `coverage` → `verify:build-gate` → `smoke` → Storybook). `npm run validate` is the fast inner-loop check and is **not** a substitute — it runs `test`, not `test:coverage`, so it does zero coverage gating.
- **Every PR references an issue**, first body line `Closes #<ISSUE_NUMBER>`.
- **Every PR carries exactly one version label**, `v1` or `v2`, matching its base branch.
- Update the relevant `README.md` / `AGENTS.md` when a change adds, removes, renames, or repurposes a file or folder, changes the structure or tech stack, or introduces a command, dependency, or architectural pattern.
## What to prioritize in review
1. Correctness and security — this backend spawns local processes and proxies outbound requests, so anything touching auth, origin validation, host binding, or the proxy's SSRF controls deserves close reading.
2. Type-safety violations (`any`, suppressions, unjustified double casts).
3. Missing or thin tests against the ≥90% four-dimension gate, and modules placed outside the gated directories.
4. Mantine convention violations — inline styles, raw colors, unnecessary CSS classes, missing `.withProps()` extraction.
5. Docs that contradict the change.

93
.github/dependabot.yml vendored Normal file
View file

@ -0,0 +1,93 @@
version: 2
# Version updates target `v2/main`, the develop branch where all v2 work lands,
# not the default branch — `main` is release-only, holding the latest released
# v2 and receiving milestone merges from `v2/main`. (The deprecated v1 line
# lives on `v1/main` and takes security fixes only.)
#
# Two things to know about this file:
# 1. Dependabot reads it from the DEFAULT branch (`main`). Changes here are
# inert until the next milestone merge carries them there.
# 2. `target-branch` scopes VERSION updates. Dependabot SECURITY updates are
# enabled in repo settings, not here, and are raised against the default
# branch — they kept working while this file was missing entirely
# (see #1833, #1840). Per GitHub's Dependabot options reference, an entry
# whose `target-branch` names a non-default branch is NOT applied to
# security updates, so the schedule/labels/groups below shape version
# updates only. Re-confirm security PRs still appear after this lands.
#
# v2 is not an npm workspace: the root and each client under `clients/*` carry
# their own package.json + lockfile, so each needs its own entry.
updates:
- package-ecosystem: "github-actions"
directory: "/"
target-branch: "v2/main"
schedule:
interval: "monthly"
labels:
- "v2"
groups:
github-actions:
patterns:
- "*"
- package-ecosystem: "npm"
directory: "/"
target-branch: "v2/main"
schedule:
interval: "monthly"
labels:
- "v2"
groups:
root-dependencies:
patterns:
- "*"
- package-ecosystem: "npm"
directory: "/clients/web"
target-branch: "v2/main"
schedule:
interval: "monthly"
labels:
- "v2"
groups:
web-dependencies:
patterns:
- "*"
- package-ecosystem: "npm"
directory: "/clients/cli"
target-branch: "v2/main"
schedule:
interval: "monthly"
labels:
- "v2"
groups:
cli-dependencies:
patterns:
- "*"
- package-ecosystem: "npm"
directory: "/clients/tui"
target-branch: "v2/main"
schedule:
interval: "monthly"
labels:
- "v2"
groups:
tui-dependencies:
patterns:
- "*"
- package-ecosystem: "npm"
directory: "/clients/launcher"
target-branch: "v2/main"
schedule:
interval: "monthly"
labels:
- "v2"
groups:
launcher-dependencies:
patterns:
- "*"

View file

@ -11,19 +11,20 @@
If you've found a bug or want a feature:
→ Open an issue instead, labeled for the version you're targeting
(`v1` for `v1/main`, `v2` for `main`).
(`v1` for `v1/main`, `v2` for `v2/main`).
If you've already built the change locally:
→ Open an issue and share the PROMPT(S) you used to generate it, not a
diff. We'll reproduce it through our own workflow.
Full policy: https://github.com/modelcontextprotocol/inspector/blob/main/CONTRIBUTORS.md
Full policy: ../CONTRIBUTING.md (relative to this template — the file lives
at the repository root, on whatever branch you're reading).
Maintainers: delete this template body and describe your change normally.
-->
> **Heads up:** this repository accepts **issues, not pull requests** from
> external contributors. Please read [`CONTRIBUTORS.md`](../blob/main/CONTRIBUTORS.md)
> external contributors. Please read [`CONTRIBUTING.md`](../CONTRIBUTING.md)
> before continuing. If you're an external contributor, open an issue (and
> share the prompt you used, if you've already built the change) rather than
> this PR.

93
.github/workflows/claude.yml vendored Normal file
View file

@ -0,0 +1,93 @@
name: Claude Code
on:
issue_comment:
types: [created]
pull_request_review_comment:
types: [created]
issues:
types: [opened, assigned]
pull_request_review:
types: [submitted]
jobs:
claude:
if: |
(
(github.event_name == 'issue_comment' &&
contains(github.event.comment.body, '@claude') &&
contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.comment.author_association)) ||
(github.event_name == 'pull_request_review_comment' &&
contains(github.event.comment.body, '@claude') &&
contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.comment.author_association)) ||
(github.event_name == 'pull_request_review' &&
contains(github.event.review.body, '@claude') &&
contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.review.author_association)) ||
(github.event_name == 'issues' &&
(contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')) &&
contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.issue.author_association))
)
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
issues: read
id-token: write
actions: read
steps:
- name: Get PR details
if: |
(github.event_name == 'issue_comment' && github.event.issue.pull_request) ||
github.event_name == 'pull_request_review_comment' ||
github.event_name == 'pull_request_review'
id: pr
uses: actions/github-script@v8
with:
script: |
let prNumber;
if (context.eventName === 'issue_comment') {
prNumber = context.issue.number;
} else {
prNumber = context.payload.pull_request.number;
}
const pr = await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: prNumber
});
core.setOutput('sha', pr.data.head.sha);
core.setOutput('repo', pr.data.head.repo.full_name);
- name: Checkout PR branch
if: steps.pr.outcome == 'success'
uses: actions/checkout@v6
with:
ref: ${{ steps.pr.outputs.sha }}
repository: ${{ steps.pr.outputs.repo }}
fetch-depth: 0
- name: Checkout repository
if: steps.pr.outcome != 'success'
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Run Claude Code
id: claude
uses: anthropics/claude-code-action@v1
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
# Allow Claude to read CI results on PRs
additional_permissions: |
actions: read
# Trigger when assigned to an issue
assignee_trigger: "claude"
claude_args: |
--mcp-config .mcp.json
--allowedTools "Bash,mcp__mcp-docs"
--append-system-prompt "If posting a comment to GitHub, give a concise summary of the comment at the top and put all the details in a <details> block. When working on MCP-related code or reviewing MCP-related changes, use the mcp-docs MCP server to look up the latest protocol documentation. For schema details, reference https://github.com/modelcontextprotocol/modelcontextprotocol/tree/main/schema which contains versioned schemas in JSON (schema.json) and TypeScript (schema.ts) formats."

8
.mcp.json Normal file
View file

@ -0,0 +1,8 @@
{
"mcpServers": {
"mcp-docs": {
"type": "http",
"url": "https://modelcontextprotocol.io/mcp"
}
}
}

324
AGENTS.md
View file

@ -5,7 +5,7 @@ This is an application for inspecting MCP servers. Has three incarnations, Web,
## Project Structure
```
inspector/
v2/main/
├── clients/
│ ├── web/ # Web client (Vite + React + Mantine)
│ │ ├── src/ # Browser source (React app, hooks, components)
@ -40,10 +40,19 @@ inspector/
│ │ ├── node/ # Node-side OAuth (NodeOAuthStorage, OAuthCallbackServer,
│ │ │ # runner-interactive-oauth loopback callback flow)
│ │ └── remote/ # Remote OAuth storage (delegates to the remote server)
│ ├── client/ # Install-level client config (`client.json`): browser-safe
│ │ # parse/validate (config-parse.ts) + Node load/save
│ │ # (config.ts, node-persistence.ts), the remote backend
│ │ # (remote.ts), secrets (secrets.ts), and runner.ts.
│ │ # Consumed by both App.tsx trees (web + tui); gated by
│ │ # the web coverage `include`, tests in
│ │ # clients/web/src/test/core/client/.
│ ├── json/ # JSON utilities and parameter/argument conversion
│ │ # (xMcpHeader.ts: SEP-2243 `x-mcp-header`
│ │ # annotation scan/validation + mirrored-param
│ │ # derivation, used by the Tools tab — #1632)
│ │ # derivation, used by the Tools tab — #1632;
│ │ # plus `Mcp-Param-*` header building for the
│ │ # wire, used by both `tools/call` paths — #1846)
│ ├── logging/ # Silent pino logger singleton
│ ├── mcp/ # InspectorClient runtime + state stores
│ │ # (modernTaskSchemas.ts: SEP-2663 modern Tasks
@ -80,6 +89,16 @@ inspector/
│ # in clients/web/vite.config.ts points at build/index.js
│ # (not src/) so `getTestMcpServerPath()` returns a `.js` path.
│ # tsconfig.test.json keeps paths pointing at src for typecheck.
├── docs/ # Task-oriented guides (mcp-server-configuration.md,
│ # mcp-app-review.md, launcher-config-consolidation-plan.md,
│ # images/). Linked from the root README.
├── scripts/ # Root build/verify tooling: install-clients.mjs (the
│ # postinstall cascade), the smoke-*.mjs runners,
│ # verify-build-gate / verify-format-coverage /
│ # verify-typecheck-coverage, pack-and-verify.mjs,
│ # and lib/ shared helpers. Prettier-gated via
│ # `format:check:scripts`; its own pure parsers are
│ # unit-tested by `npm run test:scripts` (node --test).
├── specification/ # Build specification
...
```
@ -94,39 +113,23 @@ v2 is **not** an npm workspace — each client under `clients/*` keeps its own `
After installing, `npm run build` builds all clients. The launcher scripts (`npm run web` / `web:dev`) run the built launcher, so build first; for day-to-day web iteration use `cd clients/web && npm run dev`.
## Repository & Project Board
- **Repo**: https://github.com/modelcontextprotocol/inspector.git
- **Base Branches**: v2/main (active), main (v1). v1.5/main is merged into v2/main and no longer takes new work.
- **Project Boards**:
- v2 - https://github.com/orgs/modelcontextprotocol/projects/28 (active board — all current work goes here)
- v1 - https://github.com/orgs/modelcontextprotocol/projects/11 (existing inspector version, no new activity except security and bug fixes)
## Contributing
External contributions are accepted as **issues, not pull requests** —
maintainers handle design and implementation through a prompt-driven workflow.
If you've already built a change locally, share the **prompt** you used, not a
diff. See [`CONTRIBUTORS.md`](./CONTRIBUTORS.md) for the full policy.
External contributions are accepted as **issues, not pull requests** — maintainers handle design and implementation through a prompt-driven workflow.
If you've already built a change locally, share the **prompt** you used and screenshots if applicable, not a diff. See [`CONTRIBUTING.md`](./CONTRIBUTING.md) for the full policy.
**This applies to org members with write access too, not just outside contributors.** Having permission to push a branch is not authorization to open a PR. Pull requests against this repo are opened by the **repo maintainers** only. Anyone else — including organization members whose write access makes it technically possible — opens a **detailed issue** instead, and a maintainer takes it from there. A detailed issue means: the problem, how to reproduce it, the behavior you expected, and — if you've already prototyped a fix — the prompt you used and any screenshots, rather than a diff.
**Issues are filed through the forms in [`.github/ISSUE_TEMPLATE/`](./.github/ISSUE_TEMPLATE) — blank issues are disabled.** GitHub serves the chooser from the **default branch** only, so a form edited here on `v2/main` has no effect on the live chooser until the next milestone merge into `main` — and it cannot be previewed before then, which is why the schema notes below matter. There are two forms, **Bug report** (`1-bug_report.yml`, auto-labels `bug`) and **Feature request** (`2-feature_request.yml`, auto-labels `enhancement` **and `v2`**); `config.yml` holds the chooser's contact links. A form's `labels:` is **static** — GitHub cannot map a reporter's answer to a label — which splits the two cases: the **bug** form could target either line, so it carries a required version-line *dropdown* and a maintainer applies the matching label at triage per [Label by version](#issue-driven-work-style); the **feature** form is v2 by construction (v1 takes security fixes only and cannot receive a feature), so it needs no dropdown and declares `v2` statically. If v1 ever reopens to features, that static label is what has to change. **There is deliberately no security template**: a vulnerability report must not open a public issue, so the chooser routes it to the private advisory form as a contact link instead (see [`SECURITY.md`](./SECURITY.md)). When adding or changing a form, validate it against GitHub's issue-forms schema (`markdown` blocks take no `id` and no `validations`; `checkboxes` mark `required` per option, not under `validations`).
**Every PR must reference an issue. No exceptions, regardless of who opens it.** The PR body's first line is `Closes #<ISSUE_NUMBER>` (see the [Issue-driven Work Style](#issue-driven-work-style) rules below). A PR with no linked issue has no board card, so the work is invisible to the project board and untracked — if you're about to open one and there's no issue yet, create the issue first. This holds for a maintainer's own one-line fix as much as for a feature.
## Project Status and Direction
* The main branch currently contains the legacy version of the Inspector, which we are accepting bug fixes and minor improvement PRs for.
* The v1/main branch currently contains the legacy version of the Inspector, which we are creating security fixes for in deprecated maintenance mode. It is **published straight from the branch** to the `v1-latest` npm dist-tag — v1 releases never pass through `main`, and v1 PRs therefore target `v1/main` directly.
* The v1.5/main branch was the intermediate version of the Inspector, where the shared logic between the three incarnations of the Inspector was extracted into a core subsystem with InspectorClient class as the common entry point. It also included the TUI, a refactored CLI, and streamlined launcher. The branch still exists but is **frozen** — it takes no new work. It is kept as a reference point (e.g. for tracking down a regression introduced by the merge into v2/main), so do not delete it.
* The v2/main branch currently contains the the new version of the Inspector, which is actively being developed and maintained. All new features, bug fixes, and refactors should be implemented in this branch. It acts as the **develop branch**: work accumulates here continuously and is merged into `main` at milestone releases.
* The v2/main branch currently contains the new version of the web Inspector, composed of "dumb" components which accept data and callbacks as props and contain only display logic.
The Launcher, TUI, CLI, and InspectorClient from v1.5/main have been merged into v2/main. InspectorClient is wired up to the new web Inspector. Eventually, we will replace main with v2/main, eliminating the legacy implementations.
## Web backend auth token
The dev/prod web backend protects every `/api/*` route with `x-mcp-remote-auth: Bearer <MCP_INSPECTOR_API_TOKEN>`. The browser recovers that token from three sources, in priority order (see `App.tsx` `getAuthToken()`):
1. `window.__INSPECTOR_API_TOKEN__` — injected into `index.html` on every page load by the backend (the dev Vite plugin via `transformIndexHtml`, the prod Hono server on the `/` route), both routed through `clients/web/server/inject-auth-token.ts`. This is what makes a bare-URL reload, a bookmark, or a cleared `sessionStorage` keep working.
2. `?MCP_INSPECTOR_API_TOKEN=…` query string — the URL the launcher banner prints; kept as a fallback for pasted full URLs.
3. `sessionStorage` — backstop for navigations that land without either of the above.
Injection is a no-op when auth is disabled (`DANGEROUSLY_OMIT_AUTH`), and the global name is the shared `INSPECTOR_API_TOKEN_GLOBAL` constant in `core/mcp/remote/constants.ts`.
* The main branch is the default branch for the repo, and it currently points to the latest v2 release. It is not a development branch, and no new features or bug fixes should be implemented here. It is only used for releases of the v2 Inspector via merge from v2/main, which is what publishes the `latest` npm dist-tag.
## Maintenance Rules
@ -135,35 +138,146 @@ Injection is a no-op when auth is disabled (`DANGEROUSLY_OMIT_AUTH`), and the gl
- When the structure of the project, the tech stack, or the developer setup changes, update appropriate README.md files with the details.
- When adding new commands, dependencies, or architectural patterns, update the relevant sections of appropriate README.md files as well.
- When rules for implementation and testing change, update this file AGENTS.md
- **Mirror review-relevant changes into [`.github/copilot-instructions.md`](.github/copilot-instructions.md).** That file is what GitHub Copilot reads when it reviews a PR, and it is a hand-maintained **distillation** of this one — there is no generation step and nothing detects drift, so it goes stale silently and Copilot then reviews against rules we no longer hold.
- **AGENTS.md remains the source of truth.** Never edit `copilot-instructions.md` alone to change a rule; change it here first, then mirror.
- **Review-relevant** means anything a reviewer would cite against a diff: the TypeScript rules, the Mantine/React conventions (including the `.withProps()` rule and its exceptions), the `lib` vs `utils` split, test placement, the ≥90% coverage gate and the `v8 ignore` policy, the `renderWithMantine` requirement, and the PR hygiene rules (`Closes #N`, version label). Changing any of these means updating both files in the **same PR**.
- **Not review-relevant, and deliberately absent** from the mirror: the board recipes and their IDs, milestone and branch-naming mechanics, release and publishing procedure, and the project-structure tree. Copying those in would double the maintenance surface for content no reviewer cites.
- Keep it a **distillation, not a copy** — it is read on every review, so length has a cost. Prefer tightening the summary over pasting a section wholesale.
### Issue-driven Work Style
All work should be driven by items on the project board.
> **A v2 issue is not "created" until it is BOTH labeled `v2` AND on board #28 with a Status set.** Labeling alone is not enough — a label is a repo tag; the board is a separate org project. Applying `--label v2` does **not** add the item to the board, and adding it to the board does **not** set a Status. All three are distinct steps; do all three (see the recipes below). **Only issues go on the board — never PRs.** A PR still gets the `v2` label, but it is tracked through its linked issue's card (via `Closes #N`), not its own board item.
> **A v2 issue is not "created" until it is labeled `v2`, given a milestone, AND on board #28 with a Status *and* a Priority set.** Labeling alone is not enough — a label is a repo tag, the milestone is a release bucket, and the board is a separate org project. Applying `--label v2` does **not** add the item to the board, and adding it to the board does **not** set a Status or a Priority. All five are distinct steps; do all five (see the recipes below). **Only issues go on the board — never PRs.** A PR still gets the `v2` label, but it is tracked through its linked issue's card (via `Closes #N`), not its own board item.
- Before starting work, check the board for the relevant item.
- **Every board item is a real GitHub issue.** Do not create draft items (board cards with no issue number). If you find work that needs tracking, create an actual issue and add that to the board. Before creating a new issue, check the board for a matching item to avoid duplicates — **never create a duplicate**.
- **Assign the issue to its creator.** When you create an issue, assign it to the user it is created on behalf of (`gh issue create --assignee @me ...`, or `--assignee <login>`). Board items should never be unassigned.
- **Label by version.** New issues and PRs must carry the label matching the target board / branch:
- `main` → `v1`
- `v2/main` → `v2`
- **Label by version — every issue and every PR, no exceptions.** Each one carries **exactly one** of `v1` or `v2` at creation. There is no unlabeled state and no "decide later": an issue with neither label belongs to no version line, appears in no version-filtered query, and is effectively invisible.
- `v1` — work targeting `v1/main` (the deprecated line: security fixes only)
- `v2` — work targeting `v2/main` (active development; the default for anything new)
Set the label at **create time** — `gh issue create --label v2 ...`, `gh pr create --label v2 ...` — never by backfilling later, since unlabeled items are exactly the ones missed when filtering by version. **If the target version isn't obvious, it's `v2`**: v2 is where all new work goes, and `v1` is reserved for the narrow case of patching the deprecated line. Only ask when the issue is specifically a fix *for released v1 behavior* and it's unclear whether v2 still has the bug. Note the label is a repo tag and is **not** the board — see the callout above; a `v2` issue also needs a board card with a Status **and** a Priority (a `v1` one needs a Status; board #11 has no Priority field).
- **Prioritize every new issue.** Every new issue must have a Priority (Urgent, High, Medium, or Low) set at creation time. Priority is a **board field**, not a label, so it lives on the card and an unboarded issue has nowhere to store it. Derive it with the rubric in [Setting issue priority](#setting-issue-priority) rather than asserting it — an unscored "this feels urgent" is exactly what the rubric exists to replace.
- **Add the issue to the board and set Status and Priority.** After creating an issue, add it to the board for its version — **`v2` → board #28**, **`v1` → board #11** — and set the fields. (PRs are never added to either board — they're tracked through their linked issue's card.) This is the step most easily forgotten because it needs several IDs — copy the recipes below verbatim, and take them from the section for the right board; the two projects' ids are not interchangeable.
- **New and untriaged → `Incoming`.** This is the **default status for a new item on either board.** An issue nobody has evaluated yet belongs in **Incoming**, not Todo. Todo means a maintainer approved it and it is ready to be picked up; using Todo as the inbox erases that distinction and quietly promotes unreviewed work into the queue. Anything filed by an outside reporter starts in Incoming. Work you are starting immediately goes straight to In Progress.
- **Priority is v2-only.** Board #28 has a Priority field; board #11 does not. A v1 issue gets a Status and nothing else.
- **Every new issue gets a milestone — no exceptions.** Set it at create time with `gh issue create --milestone <title> ...`. **If the user didn't specify one, default to the current milestone**: the open milestone with the nearest due date. Never leave an issue unmilestoned pending a decision — an unmilestoned issue drops out of release planning silently, the same way an unlabeled one drops out of version filtering. Moving it later is one command; noticing it was never set is the hard part. Get the current milestone with:
Set the label at create time (`gh issue create --label v2 ...`, `gh pr create --label v2 ...`) — don't rely on backfilling later, since unlabeled PRs are easy to miss when filtering by version.
- **Add the issue to the board and set Status.** After creating an issue, add it to board #28 and set its Status. (PRs are never added to the board — they're tracked through their linked issue's card.) This is the step most easily forgotten because it needs several IDs — copy the recipes below verbatim.
- When work begins, create a feature branch and set the item's Status to **In Progress** (or **V2 Go Live** for a card in the go-live phases, #1804).
```sh
# Open milestones, soonest due date first — the first row is the current one.
gh api repos/modelcontextprotocol/inspector/milestones --jq \
'map(select(.state=="open")) | sort_by(.due_on) | .[] | "\(.title)\tdue \(.due_on[0:10])\topen=\(.open_issues)"'
```
Milestones are **release** buckets (`v2.1.0`, `v2.2.0`, …), so pick by *when the work ships*, not by size. If a new issue plainly can't make the current milestone, say so and put it in the next one rather than leaving it blank. Sub-issues normally inherit their parent's milestone — if a sub-task must ship with its parent, they belong in the same one.
- When work begins, create a feature branch and set the item's Status to **In Progress**.
- **Branch names start with the target version segment.** The first path segment must be the version whose base branch the PR targets — `v2/` for work on `v2/main`, `v1/` for work on `v1/main` — followed by the usual type and slug: `v2/ci/restore-claude-workflow`, `v2/fix/oauth-scope-union`, `v1/fix/proxy-ssrf-pin`. Not `ci/restore-claude-workflow`. This keeps the two lines legible in `git branch -a` and in the PR list once v1 and v2 branches coexist on the same remote, and it matches the base branches themselves (`v2/main`, `v1/main`).
- When work is complete:
- Run format, lint, typecheck, build, and test — ensure all checks pass
- Open a PR against the matching base branch (`main` for v1, `v2/main` for v2) and set the item's Status to **In Review**
- **Link the PR to its issue.** The PR body's **first line must be `Closes #<ISSUE_NUMBER>`**. ⚠️ Note: closing keywords only auto-link/auto-close for PRs targeting the repo's **default branch** (`main`). Because v2 PRs target `v2/main` (a non-default branch), `Closes #N` there is only a cross-reference — it will **not** create a hard link or close the issue on merge. (There is no `gh` flag for manual linking — `gh pr edit` has no `--add-issue`; closing keywords are the only mechanism GitHub exposes, and they're gated to the default branch.)
- **On merge of a v2 PR, manually close its issue and move the board item to Done** (option id `248a3910`), since auto-close won't fire on `v2/main`. Keep the `Closes #N` line anyway so the issues close automatically if/when `v2/main` is eventually merged to `main`.
- Run `npm run ci` from the root — the mandatory pre-push gate (see [Mandatory pre-push gate](#mandatory-pre-push-gate)). `npm run validate` is the fast inner-loop check and is **not** a substitute: it runs no coverage gate, no smokes, and no Storybook tests.
- Open a PR against the matching base branch (`v1/main` for v1, `v2/main` for v2) and set the item's Status to **In Review**
- **Attach screenshots as proof of functionality.** Any change to the web UI or the TUI must show its result: capture before/after screenshots (or a short GIF for an interaction) and put them in a **`pr-screenshots/` folder off the repo root**, creating it if it doesn't exist. That folder is **gitignored** — the images are working artifacts staged for upload, never committed to the source tree — so attach them to the PR body from there rather than referencing an in-repo path. Name them for what they show (`tools-tab-before.png`, `tools-tab-after.png`), not `Screenshot 2026-07-31 at 14.02.11.png`.
- **Link the PR to its issue — mandatory for every PR, from anyone.** No PR is opened without an issue to reference; if one doesn't exist yet, create it first (labeled and on the board) rather than opening the PR and backfilling. Note also that only the **repo maintainers** open PRs at all (see [Contributing](#contributing)) — everyone else files a detailed issue. The PR body's **first line must be `Closes #<ISSUE_NUMBER>`**. ⚠️ Note: closing keywords only auto-link/auto-close for PRs targeting the repo's **default branch** (`main`). Because v2 PRs target `v2/main` (a non-default branch), `Closes #N` there is only a cross-reference — it will **not** create a hard link or close the issue on merge. (There is no `gh` flag for manual linking — `gh pr edit` has no `--add-issue`; closing keywords are the only mechanism GitHub exposes, and they're gated to the default branch.)
- **On merge of a v2 PR, manually close its issue and move the board item to Done** (option id `259d6aab`), since auto-close won't fire on `v2/main`. Keep the `Closes #N` line anyway so the issues close automatically if/when `v2/main` is eventually merged to `main`.
- If new tasks are discovered or requested during development, create issues and add them to the board.
## Setting issue priority
Every issue gets a **Priority on its board card**, set when you add the issue to the board. Score it rather than assert it: rate two axes 1–5, add the signal bonuses, and read the total off the band table. The point is that two people triaging the same issue land in the same place, and that the reasoning survives in a form someone can argue with later.
> ⚠️ **There are two different "Priority" fields on an issue page, and they are unrelated. Ours is the one under _Projects → Inspector V2_.**
>
> | Where it appears | What it is | Ours? |
> | --- | --- | --- |
> | **Projects → Inspector V2 → Priority** | The **project board** field on board #28 (`PVTSSF_lADOCt2Azc4BJVxtzg5iJE4`). Urgent/High/Medium/Low, each option carrying its rubric band in the description. | ✅ **Yes — this is the one this rubric sets.** |
> | **Fields → Priority** (above _Projects_) | A GitHub **issue field**, `IFSS_kgDOAdAWeg`. Defined at the **`modelcontextprotocol` org** and shared by every repo in it (typescript-sdk, servers, registry, …), alongside `Effort`, `Start date`, and `Target date`. Created 2026-05-06, `ORG_ONLY`. | ❌ No. Not ours, not repo-scoped. |
>
> They look identical — same name, same four option names — but **nothing syncs them.** Setting one does not set the other, and they will happily disagree (this was first noticed on #1891 showing `Urgent` in Fields and `High` on the board). There is no pass-through, in either direction.
>
> **Never delete the org-level field.** It belongs to the whole org, so removing it would strip Priority from every other `modelcontextprotocol` repo.
>
> **Don't set it either — but do read it.** A value there is a *reporter's* opinion, not a maintainer's assessment, so it is **untrusted input**. It feeds the rubric as a capped +1 signal bonus and nothing more; see [Trust boundary](#trust-boundary-who-can-set-what) below.
**Axis 1 — Severity / impact (1–5).** How bad is it when it happens?
| Score | Means |
| --- | --- |
| 1 | Cosmetic — a typo, a misaligned control, a wording nit. |
| 2 | Minor friction with an easy workaround. |
| 3 | A real feature is broken or missing; the workaround is annoying or partial. |
| 4 | A core workflow is unusable, or the Inspector reports something false about the server under test. |
| 5 | Data loss, a security vulnerability, or a release that is broken on arrival for everyone. |
**Axis 2 — Urgency / staleness (1–5).** How time-sensitive or neglected is it?
| Score | Means |
| --- | --- |
| 1 | No time pressure; nothing waits on it. |
| 2 | Wanted eventually. |
| 3 | Wanted this milestone, or has sat >90 days with no activity. |
| 4 | Blocking other work, or tied to a dated external dependency (an SDK release, a spec deadline). |
| 5 | Blocking a release, or actively hurting users on a published version right now. |
**Signal indicators (bonuses, +1 each — not an axis of their own).** These are corroborating evidence that the two axes may have undercounted, so they adjust the total rather than standing alone:
- Carries a `bug` or security-related label
- Linked to a milestone
- High engagement (many comments or reactions)
- Assigned to someone
- A sub-issue of a larger epic
- The reporter set `Fields → Priority` to **Urgent or High** — **+1, flat, whichever of the two they picked.** It does not map to a band, and `Urgent` earns exactly what `High` earns. See below.
**Bands.** Axes give 2–10 and there are six bonuses, so the total runs 2–16.
| Total | Priority | Meaning |
| --- | --- | --- |
| 12+ | **Urgent** | Drop what you're doing. |
| 9–11 | **High** | Next up after current work. |
| 6–8 | **Medium** | Scheduled normally. |
| ≤5 | **Low** | Nice to have; may sit. |
Note that severity alone doesn't reach Urgent: a 5/5 with no corroborating signals totals 10 and lands **High**. That's deliberate — Urgent is reserved for a severe problem that something *else* also confirms is burning, and a band that everything qualifies for stops carrying information. Override the band when it's plainly wrong, but say why in the issue; a rubric nobody may overrule is a rubric people route around.
Set the resulting level on the board card with the Priority recipe in the [V2 board (#28) `gh` recipes](#v2-board-28-gh-recipes) below.
### Trust boundary: who can set what
**The boards are private** (`public: false`, both #28 and #11 — verified 2026-08-01). The Status and Priority a maintainer assigns are visible only to people with project access: a reporter cannot see them, cannot set them, and will never learn how their issue was scored. Board priority is a maintainers' working queue, not a published commitment.
The org-level `Fields → Priority` is the opposite. It renders on the public issue page and is **not** part of maintainer triage, so any value there is **untrusted** — we didn't put it there, and it carries a preference rather than an assessment.
That asymmetry is the whole reason the reporter's value earns a flat +1 and nothing more:
- **It counts for something.** Someone flagging their own issue is real information about how much it hurts them. Discarding it throws away a signal we'd otherwise have to infer.
- **It cannot decide an outcome.** The bonus is capped, identical for `Urgent` and `High`, and can lift an issue at most one band. Nothing a reporter can type reaches Urgent by itself: Urgent needs 12, so the issue must already sit at 11 on maintainer-assessed axes — at which point the reporter is not the reason.
- **Never map the value across.** A reporter selecting `Urgent` does **not** make the board card Urgent. Doing that would hand queue position to anyone with a GitHub account, and the queue would sort by assertiveness instead of impact.
Don't lean on GitHub's permission gate to enforce this. Whether an outside reporter can set that field today is an implementation detail that can change without notice; the rule holds either way, because it rests on *who assessed the issue* rather than on who was technically able to click.
**Assess board Priority at boarding time**, from the issue as it stands. The reporter's value is one input among several, weighted as above.
## Repository & Project Boards
- **Repo**: https://github.com/modelcontextprotocol/inspector.git
- **Base Branches** — three branches, three distinct roles. Target the one matching the work; never open a PR against `main`.
| Branch | Role | PRs target it? | Publishes to |
| --- | --- | --- | --- |
| `v2/main` | **Develop.** All active v2 work lands here. | **Yes** — every v2 PR | nothing directly; reaches npm via `main` |
| `main` | **Release.** The repo's default branch; holds the latest released v2. Not a development branch. | **No** — it only receives milestone merges from `v2/main` | `latest` |
| `v1/main` | **Maintenance.** The deprecated v1 line, security fixes only, no active development. | **Yes** — every v1 PR, directly | `v1-latest`, published straight from this branch |
So v2 flows `feature branch → v2/main → (milestone) main → npm latest`, while v1 is flat: `feature branch → v1/main → npm v1-latest`, with no merge into `main` at any point. The two lines are published independently under separate dist-tags, which is why a v1 fix does **not** need to be forward-ported to reach users on v1 (`npx @modelcontextprotocol/inspector@v1-latest`).
- **Project Boards**:
- v2 - https://github.com/orgs/modelcontextprotocol/projects/28 (active board — all new work goes here)
- v1 - https://github.com/orgs/modelcontextprotocol/projects/11 (legacy inspector version, no new activity except security fixes)
**Both boards start new items in `Incoming`.** A card only leaves Incoming when a maintainer has looked at it and approved the work — that is what Todo means on either board. The two boards are otherwise separate projects with their own field and option ids; never reuse one board's ids against the other (they are rejected with "option Id does not belong to the field", so the mistake is at least loud).
#### V2 board (#28) `gh` recipes
The board is an **org project**, so all commands use `--owner modelcontextprotocol` and the numeric project `28`. The project node id and Status field id are stable. **The Status *option* ids are NOT stable — they are regenerated whenever the Status field's option list is edited** (see the ⚠️ hazard below). If any option id here is rejected, re-fetch the current set with:
The board is an **org project**, so all commands use `--owner modelcontextprotocol` and the numeric project `28`. The project node id and the field ids are stable. **The *option* ids are NOT stable — they are regenerated whenever a single-select field's option list is edited** (see the ⚠️ hazard below). If any option id here is rejected, re-fetch the current set with:
```sh
# Swap "Status" for "Priority" to fetch the other field's options.
gh project field-list 28 --owner modelcontextprotocol --format json \
| jq '.fields[] | select(.name=="Status") | .options'
```
@ -172,24 +286,71 @@ gh project field-list 28 --owner modelcontextprotocol --format json \
| --- | --- |
| Project node ID | `PVT_kwDOCt2Azc4BJVxt` |
| Status field ID | `PVTSSF_lADOCt2Azc4BJVxtzg5iI8c` |
| Priority field ID | `PVTSSF_lADOCt2Azc4BJVxtzg5iJE4` |
Status option IDs (`--single-select-option-id`) — **last verified 2026-07-27**. `V2 Go Live` was added via the web UI on 2026-07-27 for the go-live phases (#1804); the other four IDs were unchanged by that addition, confirming the web-UI path is safe (see the ⚠️ hazard below). Removed columns whose IDs are now rejected: `SDK V2 + New Spec` (`1bbb6f57`), `Building CLI / TUI / CORE` (`4ac261ee`), `Building Web` (`c28da89f`), `MCP Apps Extension` (`73d0b807`).
Status option IDs (`--single-select-option-id`) — **last verified 2026-08-01**.
| Status | Option ID |
| --- | --- |
| Incoming | `721a3d4c` |
| Todo | `fbdaf21e` |
| V2 Go Live | `b3a6966e` |
| In Progress | `195df262` |
| In Review | `159c8a02` |
| Done | `248a3910` |
| Done | `259d6aab` |
Use **Todo** for approved-but-not-started work, **In Progress** for general active work (regardless of surface), **V2 Go Live** for cards in the go-live phases (#1804), **In Review** once a PR is open, and **Done** on merge.
Use **Incoming** for newly filed, untriaged work, **Todo** once a maintainer has approved it and it's ready to pick up, **In Progress** for general active work (regardless of surface), **In Review** once a PR is open, and **Done** on merge. The Incoming/Todo line is the one that matters: Todo asserts approval, so an unreviewed issue parked there is a false claim that someone signed off on it.
> ⚠️ **Never add, rename, or remove a board column (Status option) with the `updateProjectV2Field` GraphQL mutation unless you pass every existing option's `id`.** That mutation does a **full replace** of the option list: if you resend options by name/color/description but omit their `id`s, GitHub **deletes all existing options and mints new ones**, which **orphans the Status of every card on the board** (all items go blank) *and* invalidates every option id in the table above. This has happened once (required reconstructing ~197 items' statuses by inference). Safe alternatives, in order of preference:
> 1. **Add/rename/remove a column in the GitHub web UI** (Project #28 → Status field settings). This preserves ids of untouched options and never orphans cards.
> 2. If you must script it, first `gh api graphql` the current options **with their `id`s**, then call `updateProjectV2Field` echoing back every existing option **including its `id`**, appending only the new one. Verify afterward that no card lost its Status.
Priority option IDs (`--single-select-option-id`) — **last verified 2026-08-01**. Derive the level with the rubric in [Setting issue priority](#setting-issue-priority); don't eyeball it.
| Priority | Option ID | Rubric total |
| --- | --- | --- |
| Urgent | `79628723` | 12+ |
| High | `0a877460` | 9–11 |
| Medium | `da944a9c` | 6–8 |
| Low | `d67ac7ce` | ≤5 |
> ⚠️ **Never add, rename, or remove an option on a single-select board field (Status or Priority) with the `updateProjectV2Field` GraphQL mutation unless you pass every existing option's `id`.** That mutation does a **full replace** of the option list: if you resend options by name/color/description but omit their `id`s, GitHub **deletes all existing options and mints new ones**, which **orphans that field's value on every card on the board** (all items go blank for the field you edited — Status if you were editing Status, Priority if you were editing Priority) *and* invalidates every option id in that field's table above. This has happened once, on Status (required reconstructing ~197 items' statuses by inference). Safe alternatives, in order of preference:
> 1. **Add or rename an option in the GitHub web UI** (Project #28 → the field's settings). This preserves ids of untouched options and never orphans the cards on *other* options. ⚠️ **Deleting is different, in the UI as much as in the API: removing an option blanks that field's value on every card that held it, with no undo and no warning that says so.** Before deleting any option, snapshot the board (see recovery below).
> 2. If you must script it, first `gh api graphql` the current options **with their `id`s**, then call `updateProjectV2Field` echoing back every existing option **including its `id`**, appending only the new one. `ProjectV2SingleSelectFieldOptionInput.id` is an optional `String`, so a mixed list works: echo the `id` for every option that already exists, omit it only for the one being added. Verify afterward that no card lost its value — snapshot `gh project item-list … --format json` before and after and diff, don't just spot-check.
>
> `gh project item-add` and `gh project item-edit` are always safe — they set a card's value and never touch the field schema. When option ids change for any reason, **re-verify and update the table above** (and the `248a3910` / `195df262` / `159c8a02` references in the recipes below and the merge step above).
> Both the `Incoming` Status option and the Urgent/High/Medium/Low `Priority` options were added this way (#1891), with the before/after diff confirming all 264 cards kept their Status.
>
> `gh project item-add` and `gh project item-edit` are always safe — they set a card's value and never touch the field schema. When option ids change for any reason, **re-verify and update the table above** (and the references in the recipes below and the merge step above).
>
> ### Always snapshot before touching a field's options
>
> One command, and it is the difference between a five-minute restore and reconstructing statuses by inference:
>
> ```sh
> gh project item-list 28 --owner modelcontextprotocol --format json --limit 600 > board-snapshot.json
> ```
>
> ### Recovering from a deleted option
>
> This has now happened twice — once via the API (~197 items, reconstructed by inference) and once via the UI (the `Done` column, 247 items, restored from a snapshot in minutes). With a snapshot the recovery is mechanical.
>
> **The recipe below is written for a deleted *Status* option** — it reads `.status` and writes the Status field id. For a deleted **Priority** option it is the same three steps with two substitutions: read `.priority` instead of `.status` (`gh project item-list --format json` exposes each single-select field under its lowercased name, so both keys are present), and pass the Priority field id `PVTSSF_lADOCt2Azc4BJVxtzg5iJE4` instead of the Status one. Everything else — the snapshot, the grouping safety check, the new-id caveat — applies unchanged.
>
> ```sh
> # 1. Which cards lost their value, and what did they hold?
> gh project item-list 28 --owner modelcontextprotocol --format json --limit 600 > board-broken.json
> jq -r '[.items[]|select(.status==null)|.id]' board-broken.json > lost-ids.json
> jq -r --slurpfile L lost-ids.json '($L[0]) as $lost
> | [.items[] | select(.id as $i | $lost|index($i)) | .status // "(none)"]
> | group_by(.) | map({s:.[0],c:length}) | .[] | "was \(.s): \(.c)"' board-snapshot.json
>
> # 2. Recreate the option, echoing every surviving option's id (see above).
> # NOTE: the recreated option gets a NEW id — the deleted one never comes back.
>
> # 3. Re-apply it to the orphaned cards.
> for id in $(jq -r '.[]' lost-ids.json); do
> gh project item-edit --project-id PVT_kwDOCt2Azc4BJVxt --id "$id" \
> --field-id PVTSSF_lADOCt2Azc4BJVxtzg5iI8c --single-select-option-id <NEW_OPTION_ID>
> sleep 0.4
> done
> ```
>
> Step 1's grouping is the safety check: confirm the orphaned set is exactly the cards that held the deleted option, so you don't overwrite a card someone legitimately moved in the meantime. And because the recreated option carries a **new id**, the table above and every reference to it must be updated in the same change — `grep` the old id across the repo. The `Done` id has been `248a3910` and is now `259d6aab` for exactly this reason.
```sh
# 1. Add an issue to the board — prints the item id (PVTI_…); capture it.
@ -203,23 +364,57 @@ gh project item-edit \
--single-select-option-id 195df262
```
The one-liner that does both, capturing the item id (use the option id for the status you want):
The full one-liner for a **new** issue — add it, then set Status and Priority (both are required; here Incoming + Medium):
```sh
ITEM_ID=$(gh project item-add 28 --owner modelcontextprotocol --url <issue-url> --format json --jq '.id')
gh project item-edit --project-id PVT_kwDOCt2Azc4BJVxt --id "$ITEM_ID" --field-id PVTSSF_lADOCt2Azc4BJVxtzg5iI8c --single-select-option-id 195df262
# Status → Incoming
gh project item-edit --project-id PVT_kwDOCt2Azc4BJVxt --id "$ITEM_ID" --field-id PVTSSF_lADOCt2Azc4BJVxtzg5iI8c --single-select-option-id 721a3d4c
# Priority → Medium
gh project item-edit --project-id PVT_kwDOCt2Azc4BJVxt --id "$ITEM_ID" --field-id PVTSSF_lADOCt2Azc4BJVxtzg5iJE4 --single-select-option-id da944a9c
```
For an issue **already on the board** (moving an existing card, e.g. to **In Review** when its PR opens), look its item id up by issue number instead of re-adding it. Keep `--limit` above the board's item count (~200 as of 2026-07-27) — past it `item-list` truncates silently, `select` matches nothing, and `item-edit --id ""` fails with an opaque node-resolution error rather than saying the limit was too low:
Each `item-edit` sets **one** field, so setting both takes two calls — there is no combined form.
For an issue **already on the board** (moving an existing card, e.g. to **In Review** when its PR opens, or re-scoring its Priority), look its item id up by issue number instead of re-adding it. Keep `--limit` above the board's item count (~265 as of 2026-08-01) — past it `item-list` truncates silently, `select` matches nothing, and `item-edit --id ""` fails with an opaque node-resolution error rather than saying the limit was too low:
```sh
# --limit must stay above the board's item count (~200 today) — past it the
# --limit must stay above the board's item count (~265 today) — past it the
# list truncates silently and item-edit fails with an opaque node-resolution error.
ITEM_ID=$(gh project item-list 28 --owner modelcontextprotocol --format json --limit 500 \
--jq '.items[] | select(.content.number==<ISSUE_NUMBER>) | .id')
gh project item-edit --project-id PVT_kwDOCt2Azc4BJVxt --id "$ITEM_ID" --field-id PVTSSF_lADOCt2Azc4BJVxtzg5iI8c --single-select-option-id 159c8a02
```
#### V1 board (#11) `gh` recipes
The v1 line takes **security fixes only**, so this board sees little traffic — but a v1 issue still gets a card, and it starts in **Incoming** like a v2 one. Board #11 is a separate org project with **its own ids**; none of the #28 ids above work here.
| Thing | ID |
| --- | --- |
| Project node ID | `PVT_kwDOCt2Azc4BA5sz` |
| Status field ID | `PVTSSF_lADOCt2Azc4BA5szzgzkS-g` |
Status option IDs — **last verified 2026-08-01**.
| Status | Option ID |
| --- | --- |
| Incoming | `831820cf` |
| Todo | `f75ad846` |
| In Progress | `47fc9ee4` |
| In Review | `0439b2bf` |
| Done | `98236657` |
There is **no Priority field on this board** — the priority rubric applies to v2 only. Don't try to set one here; the field id doesn't exist.
```sh
# Add a v1 issue to board #11 and put it in Incoming.
ITEM_ID=$(gh project item-add 11 --owner modelcontextprotocol --url <issue-url> --format json --jq '.id')
gh project item-edit --project-id PVT_kwDOCt2Azc4BA5sz --id "$ITEM_ID" --field-id PVTSSF_lADOCt2Azc4BA5szzgzkS-g --single-select-option-id 831820cf
```
The ⚠️ option-deletion hazard, the snapshot rule, and the recovery recipe above apply to **this board too** — same mutation, same failure mode, different ids. Note that three cards on #11 already carry no Status; that predates the `Incoming` addition (verified by before/after diff on 2026-08-01) and is not evidence of an orphaning event.
### Always test new or modified code
- Ensure all code has corresponding tests
- Ensure test coverage for each file is at least 90%
@ -227,12 +422,15 @@ gh project item-edit --project-id PVT_kwDOCt2Azc4BJVxt --id "$ITEM_ID" --field-i
- Run unit tests with `npm run test` (or `npm run test:watch` during development) from `clients/web/`
- Run CLI tests with `npm run test` from `clients/cli/` (builds test-servers + CLI bin first via `pretest`)
- Run TUI tests with `npm run test` from `clients/tui/`
- Run launcher tests with `npm run test` from `clients/launcher/`
- Run the root tooling's own tests with `npm run test:scripts` from the root — `node --test "scripts/**/*.test.mjs"`, node's built-in runner (the root has no vitest harness by design). A new `scripts/*.mjs` helper with pure logic gets a sibling `*.test.mjs`; keep the filename `*.test.mjs`, since `node --test` silently **skips** a file its glob misses and still exits 0 (`verify:typecheck-coverage` guards against exactly that).
- **The test tiers, shallowest first:** unit (`test`, per client) → web integration (`test:integration`, real transports/servers) → out-of-process (`clients/cli/__tests__/e2e.test.ts`, spawns the built binary) → smokes through the built launcher (`npm run smoke`) → Storybook play functions (`test:storybook`) → the published-tarball check (`npm run pack:verify`, local/release only — needs network). `validate` runs the per-client `test` scripts — so web **unit** plus cli's out-of-process `e2e.test.ts` (it's part of cli's `test`), but **not** web's integration project, which runs inside the `coverage` gate. Everything from `smoke` rightward is `npm run ci` only, and is described under [Mandatory pre-push gate](#mandatory-pre-push-gate).
- The repo root has no aggregate `test` script — each client self-validates, so run `npm run validate` from the root (all clients, fast) or `cd clients/<name> && npm run validate` (one client). Each client still exposes its own `test` / `test:coverage` for quick iteration.
- **`validate` is fast: it runs `test`, not `test:coverage`.** The coverage gate (slower — adds v8 instrumentation, and for web the integration project) is a **separate** top-level `npm run coverage` (and per-client `coverage:web` / `coverage:cli` / `coverage:tui` / `coverage:launcher`, each delegating to that client's `test:coverage`). Run `npm run coverage` when you want 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 … -- <reason> */` 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.
- **TUI** (`clients/tui`): the gate now covers **all of `src/**`, React surface included** — the former interim exclusion of the Ink components, `App.tsx`, and `hooks/` was lifted in #1501. Components mount through `ink-testing-library` with the `ink-scroll-view` / `ink-form` passthrough doubles in `__tests__/helpers/`, `App.tsx` mounts against a controllable mock of the `@inspector/core` surface, and keypresses are driven through stdin. The **only** coverage exclusion left in `clients/tui/vitest.config.ts` is `src/tui-servers.ts` — a pure re-export + type alias of core's server resolver with no runtime statements of its own (the logic is measured in `core/` via the web suite; `tui-servers.test.ts` still exercises it behaviorally, and it's excluded only so it doesn't surface as a misleading 0/0 row). Any new logic under `clients/tui/src`, React or not, is held to the gate automatically.
- Run `npm run test:integration` (also from `clients/web/`) for the InspectorClient + transport + auth integration suite. It runs under a separate `integration` vitest project in node env (no happy-dom) with 30s timeouts. The script builds `test-servers/` first via `tsc -p ../../test-servers --noCheck` so the stdio MCP test server can be spawned as a real subprocess. CI does not run `test:integration` as its own step — the integration project is covered by the CI `coverage` gate, whose web `test:coverage` runs `--project=unit --project=integration --coverage`.
- Test files live alongside the source as `<Name>.test.tsx` (or `.test.ts` for non-React modules). Integration tests live under `clients/web/src/test/integration/`, mirroring the `core/` source layout (`mcp/`, `mcp/node/`, `mcp/remote/`, `auth/`, `auth/node/`, `storage/`). Any test file under that folder is automatically picked up by the `integration` vitest project (node env, 30s timeouts) via the folder glob in `vite.config.ts` — placement is the manifest, there is no enumeration to keep in sync. Tests outside the folder run in the `unit` project (happy-dom). When adding a new test for, e.g., `core/mcp/remote/foo.ts`, put it at `src/test/integration/mcp/remote/foo.test.ts`.
- **Test placement: side-by-side by default, `src/test/` only for what can't be co-located.** These look like competing conventions but aren't — the split is: *tests live beside their source, **except** tests for the repo-root `core/` package (which lives outside `clients/web/`) and shared test scaffolding — both of which live under `src/test/`, with `core/` tests mirroring the `core/` layout and integration tests under `src/test/integration/`.*
@ -343,9 +541,25 @@ Nothing **enforces** the boundary: no path alias keys off it, and the coverage `
// Component.tsx
const MessageContainer = Paper.withProps({ variant: 'message' });
```
- State and effects
- **NEVER reset or re-sync local state from a prop inside a `useEffect`.** `useEffect(() => setX(prop), [prop])` renders once with the stale value, paints it, and only then corrects itself — the user sees the wrong frame and React renders twice. It is an error under `react-hooks/set-state-in-effect`, which the web client's `eslint-plugin-react-hooks` recommended set enforces.
- Use **`useValueChange(value, onChange)`** (`clients/web/src/hooks/useValueChange.ts`) instead. It is React's documented ["adjusting state during render"](https://react.dev/reference/react/useState#storing-information-from-previous-renders) pattern: it compares `value` against the previous render's with `Object.is` and calls `onChange(next)` during render, so React discards the in-progress output and re-runs the component before anything reaches the DOM. It does **not** fire on the first render — seed the dependent state with `useState` instead. Because the comparison is `Object.is`, the value you pass **must be referentially stable** across renders that mean "no change": prefer a primitive key derived from the data (an id, a name, a URI), and otherwise a memoized value. A fresh object/array literal would compare unequal every render and loop.
- The `onChange` you pass runs **during render**, so it must be pure — `setState` calls and nothing else. No fetches, DOM writes, logging, ref mutation, or parent callbacks: a render can be replayed (StrictMode) or abandoned (concurrent React), so external work would run an unpredictable number of times.
- An effect is still the right tool for genuine synchronization with an external system (DOM measurement, `requestAnimationFrame`, subscriptions, timers). The rule is about deriving React state from React props, not about effects in general. `NetworkEntry` shows the split: the reveal's force-open is a state update and uses `useValueChange`, while its `requestAnimationFrame` scroll stays a `useEffect`.
- Theme files vs. Storybook element components
- **Theme files** (`src/theme/<Component>.ts`) and **element components** (`src/components/elements/`) serve different purposes and both are needed.
- Theme files customize every instance of a Mantine component app-wide — defaults (size, radius), custom variants, and global style overrides. They are applied automatically by `MantineProvider`.
- Element components add domain-specific semantics on top of Mantine primitives. For example, `AnnotationBadge` maps domain concepts (audience, destructive, longRun) to Mantine's styling primitives (color, variant). Storybook documents these domain components for designers and developers.
- Element components MUST import from `@mantine/core`, NOT from `src/theme/`. The theme layer is applied transparently by the provider — elements do not need to know about `Theme<Name>` constants.
- NEVER push domain-specific variant logic (e.g., annotation types, transport types) into theme files. Domain variants belong in the element component that owns those semantics. Theme files are for styling that applies to the Mantine primitive globally.
## Web backend auth token
The dev/prod web backend protects every `/api/*` route with `x-mcp-remote-auth: Bearer <MCP_INSPECTOR_API_TOKEN>`. The browser recovers that token from three sources, in priority order (see `App.tsx` `getAuthToken()`):
1. `window.__INSPECTOR_API_TOKEN__` — injected into `index.html` on every page load by the backend (the dev Vite plugin via `transformIndexHtml`, the prod Hono server on the `/` route), both routed through `clients/web/server/inject-auth-token.ts`. This is what makes a bare-URL reload, a bookmark, or a cleared `sessionStorage` keep working.
2. `?MCP_INSPECTOR_API_TOKEN=…` query string — the URL the launcher banner prints; kept as a fallback for pasted full URLs.
3. `sessionStorage` — backstop for navigations that land without either of the above.
Injection is a no-op when auth is disabled (`DANGEROUSLY_OMIT_AUTH`), and the global name is the shared `INSPECTOR_API_TOKEN_GLOBAL` constant in `core/mcp/remote/constants.ts`.

128
CODE_OF_CONDUCT.md Normal file
View file

@ -0,0 +1,128 @@
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, religion, or sexual identity
and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the
overall community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or
advances of any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email
address, without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
mcp-coc@anthropic.com.
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series
of actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or
permanent ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within
the community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.0, available at
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
Community Impact Guidelines were inspired by [Mozilla's code of conduct
enforcement ladder](https://github.com/mozilla/diversity).
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see the FAQ at
https://www.contributor-covenant.org/faq. Translations are available at
https://www.contributor-covenant.org/translations.

154
CONTRIBUTING.md Normal file
View file

@ -0,0 +1,154 @@
# Contributing to MCP Inspector
Thank you for your interest in improving the MCP Inspector. Contributors are
genuinely valued — the goal of this document is to channel your input into a
form we can act on quickly and consistently.
## TL;DR
**We accept issues, not pull requests.** Design and implementation are done by
the maintainers. If you've already built a fix or feature locally, share **the
prompt you used** to produce it — not the source code. This applies to everyone
outside the repo maintainers, including organization members who happen to have
write access to this repository.
## Why this policy exists
The Inspector v2 is developed with an AI-assisted, prompt-driven workflow built
around a consistent architecture, a shared design system, and strict testing
gates (see [`AGENTS.md`](./AGENTS.md)). Every component follows the same
conventions: "dumb" components that take data and callbacks as props, Mantine
for all UI, theme variants instead of ad-hoc CSS, and a uniform per-file
coverage gate of ≥ 90% on lines, statements, functions, and branches.
Accepting raw source PRs creates friction: a diff written outside this pipeline
has to be reverse-engineered to fit our component/theme conventions, coverage
gates, and review process — often it's faster to re-derive the change than to
adapt the patch. Capturing your **intent** (a well-formed issue) or the
**prompt** that generated your local change lets us reproduce the work inside
our own workflow and standards, with the quality bar already baked in.
This policy is about efficiency, not gatekeeping. Your ideas, bug reports, and
prompts directly shape what gets built. And if you'd like to contribute more
than an issue, see
[Want to work on the Inspector with us?](#want-to-work-on-the-inspector-with-us)
below — we'd rather bring you into the workflow than turn you away.
## Who opens pull requests
Pull requests against this repository are opened by the **repo maintainers**
only. That includes organization members with write access: being able to push
a branch here isn't the same as being asked to — the constraint is the workflow
described above, not permissions, so the same policy applies whether or not
GitHub would let you click the button.
If you're not a repo maintainer, open a **detailed issue** instead and a
maintainer will pick it up. If you've already prototyped the change locally,
say so in the issue and include the prompt you used (see
[If you've already fixed it locally](#if-youve-already-fixed-it-locally)) —
that's the fastest path from your work to a merged change.
**Every pull request references an issue**, including the maintainers' own. The
PR body's first line is `Closes #<ISSUE_NUMBER>`. Work is tracked on the project
board through issues, so a PR without one is invisible to the board — which is
why a well-formed issue is the useful contribution here, and why writing one is
never wasted effort.
## How to contribute a bug report or feature request
Open a well-formed issue describing the bug or the feature you have in mind.
A great issue gives us everything we need to act on it without a round-trip —
see [What makes a good issue or prompt submission](#what-makes-a-good-issue-or-prompt-submission)
below. That's the whole process: you describe the intent, we handle the design
and implementation.
[**New issue**](https://github.com/modelcontextprotocol/inspector/issues/new/choose)
offers a **Bug report** and a **Feature request** form. Blank issues are
disabled, so pick one of the two. The bug form requires the facts triage needs
first — which client, which version line, which transport; the feature form
asks for the client and the problem you are trying to solve, and targets v2
only.
(GitHub serves the chooser from the repository's **default branch**, so what
you see when filing is whatever has reached `main`; a form added on `v2/main`
appears at the next milestone merge.) The same chooser links out to the private
security-advisory process, to this policy, and to the specification and SDK
repositories for reports that aren't about the Inspector itself.
### Which version and label?
The Inspector is maintained across two versions, each with its own base branch
and version label. File your issue against the version your report or request
targets:
| Version | Base branch | Label | npm tag |
| ------- | ----------- | ----- | ------------ |
| v1 | `v1/main` | `v1` | `v1-latest` |
| v2 | `v2/main` | `v2` | `latest` |
- **v1** (`v1/main`) is the legacy Inspector — it takes **security fixes
only**, and is published straight from that branch to the `v1-latest` npm
tag (`npx @modelcontextprotocol/inspector@v1-latest`).
- **v2** (`v2/main`) is where all current work happens — when in doubt, target
v2. `v2/main` is the develop branch; it is merged into `main` at milestone
releases, and `main` is what publishes the `latest` npm tag. Nothing targets
`main` directly.
**Label by version.** Every issue (and the PRs maintainers open for it) must
carry the label matching the target branch — `v1` for `v1/main` and `v2` for
`v2/main`. This mirrors the "Label by version" convention documented in
[`AGENTS.md`](./AGENTS.md).
## If you've already fixed it locally
Please don't send a diff or open a pull request. Instead, open an issue that
includes:
- **The prompt(s) you used** to generate the change — the exact text, so we can
reproduce it through our own workflow.
- **A description of the behavior before and after** your change.
- **How you verified it** (steps you ran, tests you added, what you observed).
We'll reproduce the change through our pipeline so it lands with the right
conventions, tests, and coverage.
## What makes a good issue or prompt submission
A great submission gives us everything we need to act without a round-trip:
- **Clear reproduction or use case** — exact steps to reproduce a bug, or a
concrete description of the feature and the problem it solves.
- **Expected vs. actual behavior** — what you saw, and what you expected
instead.
- **Affected client** — which incarnation is involved: **Web**, **TUI**, or
**CLI** (or "all" / "core" if it's shared logic).
- **Environment details** when relevant — OS, Node version, the MCP server you
were inspecting, and any relevant config.
- **The exact prompt text**, if you generated a local change and want us to
reproduce it.
## Want to work on the Inspector with us?
The issues-only policy is about how **unsolicited patches** are handled — it is
not a closed door. If you want to contribute at a deeper level, either in
general or in a specific area, we'd genuinely like to hear from you:
- **Join the [MCP Contributor Discord](https://discord.gg/6CSzBmMkjX)** and say
hello in **`#inspector-dev`**. That's where day-to-day Inspector development
is discussed and the fastest way to reach the maintainers.
- **Attend the community calls** at
[meet.modelcontextprotocol.io](https://meet.modelcontextprotocol.io/) — see
[Contributor Communication](https://modelcontextprotocol.io/community/communication)
for the full set of channels and how they're used.
From there we can scope a piece of work with you and supervise it through our
workflow — which is also the path toward becoming a maintainer. What we want to
avoid is drive-by diffs that have to be reverse-engineered into our pipeline;
what we want to encourage is sustained, coordinated contribution.
All participation is governed by the MCP
[Code of Conduct](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/CODE_OF_CONDUCT.md).
## Questions
If you're unsure how to scope something, open the issue anyway and say so —
we'll help shape it. Thanks for helping make the Inspector better.

View file

@ -14,7 +14,7 @@ npx @modelcontextprotocol/inspector --cli # CLI
npx @modelcontextprotocol/inspector --tui # TUI
```
> **Repo status.** This is the **v2** line of the Inspector (branch `v2/main`). The `main` branch is the legacy v1 implementation (bug fixes only). v2 will eventually replace `main`. See [`AGENTS.md`](./AGENTS.md) for branch/board conventions.
> **Repo status.** This is the **v2** line of the Inspector. Active development happens on **`v2/main`** (the develop branch — all v2 PRs target it), which is merged into **`main`** at milestone releases; `main` is the default branch and holds the latest released v2, published to the npm `latest` tag. The legacy **v1** line lives on **`v1/main`** — security fixes only, published straight from that branch to the npm `v1-latest` tag (`npx @modelcontextprotocol/inspector@v1-latest`). See [`AGENTS.md`](./AGENTS.md) for branch/board conventions.
## Project layout
@ -29,6 +29,7 @@ inspector/
│ └── launcher/ # Shared launcher — provides the `mcp-inspector` bin, dispatches to web/cli/tui
├── core/ # Shared code consumed via the `@inspector/core` alias (no package.json)
│ ├── auth/ # OAuth: providers, discovery, storage, mid-session recovery (browser/node/remote backends)
│ ├── client/ # Install-level client config (`client.json`): browser-safe parse/validate + Node load/save, remote backend, secrets
│ ├── json/ # JSON + parameter/argument conversion utilities
│ ├── logging/ # Silent pino logger singleton
│ ├── mcp/ # InspectorClient runtime, state stores, transports, config import
@ -37,6 +38,7 @@ inspector/
│ └── storage/ # File I/O helpers for the OAuth persist backends
├── test-servers/ # Composable MCP test servers + fixtures used by integration tests
├── scripts/ # Root build/verify tooling (install cascade, smokes, verify-build-gate, verify-format-coverage, pack:verify)
├── docs/ # Task-oriented guides (server configuration, MCP App review, launcher/config plan)
├── specification/ # Design/build specifications
├── AGENTS.md # Contribution rules for agents AND humans (see below)
└── README.md # You are here
@ -174,7 +176,7 @@ It also serves four `trigger_*` tools that the modern leg's spec-error injector
Open the Network tab to see the mirrored `Mcp-*` headers highlighted, sentinel values decoded, and each error rendered distinctly.
> **`Mcp-Param-*` mirroring is skipped by the SDK in the browser** (`detectProbeEnvironment() !== "browser"`). So calling `get_weather` from the **web** client omits `Mcp-Param-City` and the strict server answers `-32020`. The same tool is callable from the Node CLI/TUI, where mirroring is active.
> **`Mcp-Param-*` mirroring is built by the Inspector, not the SDK.** The SDK only mirrors inside `client.callTool()`, and skips it in the browser (`detectProbeEnvironment() !== "browser"`). The Inspector routes `tools/call` through `client.request()` to drive MRTR manually, so it builds the mirrored headers itself ([#1846](https://github.com/modelcontextprotocol/inspector/issues/1846)) — on **every** client, web included, since the web client's upstream request is issued by the Node backend rather than the browser. So `get_weather` is callable from web, CLI, and TUI alike, in both the plain and "Run as task" forms.
#### `x-mcp-header` in the Tools tab
@ -250,17 +252,17 @@ 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` | Runs `verify:format-coverage` (asserts every tracked source file is format-gated) first, then `validate:core` (the shared `core/` `format:check` + `lint` gate), 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), plus a headless-Chromium boot smoke that runs the prod web bundle and asserts a clean first render (no uncaught error — sync exception or unhandled rejection, how a Node built-in reaching the browser bundle manifests). |
| `npm run verify:build-gate` | Runs a real `vite build` with a Node built-in forced into the browser graph and asserts the build **fails** via the #1769 gate (which turns Vite's browser-externalization warning into a hard error). Guards against the warning phrasing drifting in a Vite bump and silently disabling the gate. Part of `npm run ci`. |
| `npm run verify:format-coverage` | Parses the `format:check` globs out of every `package.json` (only those reachable from `validate`), enumerates all tracked source files, and **fails** listing any not covered by a glob — the durable guard for the "every first-party source file is format-gated" invariant (#1792). Runs first in `validate`. |
| `npm run test:scripts` | Table-driven unit tests (`node --test`) for the guard's own pure parsers (`scripts/lib/npm-scripts.mjs` + the exported helpers of `verify-typecheck-coverage.mjs`), one case per rule they encode. Runs in `validate` — and `verify:typecheck-coverage` guards _this_ gate in turn (reachable from `validate`, non-empty test set, every test file matched by the `test:scripts` glob), since `node --test` silently skips a file its glob misses and still exits 0. |
| `npm run verify:typecheck-coverage` | The typecheck-coverage analog of the above (#1791): for each Node client (auto-discovered from disk — enrolled via its `typecheck` script's projects, or for a `tsc -b` client like `clients/web` via its `tsconfig.json` `references`) it runs those projects with `tsc --listFilesOnly`, unions them, and **fails** listing any tracked `.ts`/`.tsx`/`.mts`/`.cts` under the client that lands in no project (so a new top-level config/helper can't silently go untypechecked). It also requires, deny-by-default, the first-party TS no client owns (`test-servers/src`, the root `vitest.shared.mts`, all of `core/`, and any new top-level location) to land in some client project's tsc pass — so a `core` `*.tsx` web's projects don't reach is caught too. Also asserts the gate is wired (each client's typecheck pass — its `typecheck` script, or web's `tsc -b` — is reachable from its `validate`, and the root chain runs each client's `validate`). Runs in `validate`. |
| `npm run ci` | **Mandatory pre-push command.** `validate` → `coverage` → `verify:build-gate` → `smoke` → Storybook. A true superset of GitHub CI. |
| `npm run pack:verify` | Publish smoke — see [Publishing](#publishing). |
| Script | What it does |
| --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `npm run validate` | Runs `verify:format-coverage` (asserts every tracked source file is format-gated) first, then `validate:core` (the shared `core/` `format:check` + `lint` gate), 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), plus a headless-Chromium boot smoke that runs the prod web bundle and asserts a clean first render (no uncaught error — sync exception or unhandled rejection, how a Node built-in reaching the browser bundle manifests). |
| `npm run verify:build-gate` | Runs a real `vite build` with a Node built-in forced into the browser graph and asserts the build **fails** via the #1769 gate (which turns Vite's browser-externalization warning into a hard error). Guards against the warning phrasing drifting in a Vite bump and silently disabling the gate. Part of `npm run ci`. |
| `npm run verify:format-coverage` | Parses the `format:check` globs out of every `package.json` (only those reachable from `validate`), enumerates all tracked source files, and **fails** listing any not covered by a glob — the durable guard for the "every first-party source file is format-gated" invariant (#1792). Runs first in `validate`. |
| `npm run test:scripts` | Table-driven unit tests (`node --test`) for the guard's own pure parsers (`scripts/lib/npm-scripts.mjs` + the exported helpers of `verify-typecheck-coverage.mjs`), one case per rule they encode. Runs in `validate` — and `verify:typecheck-coverage` guards *this* gate in turn (reachable from `validate`, non-empty test set, every test file matched by the `test:scripts` glob), since `node --test` silently skips a file its glob misses and still exits 0. |
| `npm run verify:typecheck-coverage` | The typecheck-coverage analog of the above (#1791): for each Node client (auto-discovered from disk — enrolled via its `typecheck` script's projects, or for a `tsc -b` client like `clients/web` via its `tsconfig.json` `references`) it runs those projects with `tsc --listFilesOnly`, unions them, and **fails** listing any tracked `.ts`/`.tsx`/`.mts`/`.cts` under the client that lands in no project (so a new top-level config/helper can't silently go untypechecked). It also requires, deny-by-default, the first-party TS no client owns (`test-servers/src`, the root `vitest.shared.mts`, all of `core/`, and any new top-level location) to land in some client project's tsc pass — so a `core` `*.tsx` web's projects don't reach is caught too. Also asserts the gate is wired (each client's typecheck pass — its `typecheck` script, or web's `tsc -b` — is reachable from its `validate`, and the root chain runs each client's `validate`). Runs in `validate`. |
| `npm run ci` | **Mandatory pre-push command.** `validate` → `coverage` → `verify:build-gate` → `smoke` → Storybook. A true superset of GitHub CI. |
| `npm run pack:verify` | Publish smoke — see [Publishing](#publishing). |
Per-client scripts exist too (`validate:web`, `coverage:cli`, `smoke:tui`, …), plus root `validate:core` / `format:core` for the shared `core/` package, `format:scripts` for the root `scripts/` tooling, and `format:shared` / `lint:shared` for the root "shared" surface (`test-servers/src/**`, `vitest.shared.mts`, the root `eslint.config.js`). Run `npm run format` before committing — the root `format` fixes `core/`, the root `scripts/`, the shared surface, and every client; `validate` runs the non-fixing `format:check` and fails CI on any unformatted file.
@ -286,9 +288,11 @@ The `smoke:*` scripts run against the in-repo build tree, which is **not** the p
Publishing is automated by two release-gated jobs in [`.github/workflows/main.yml`](.github/workflows/main.yml) (`github.event_name == 'release'`, both `needs: build`):
- **`publish`** — the npm package. Runs `npm run pack:verify` as the pre-publish gate, asserts the release tag matches the root `package.json` version, then `npm publish --access public --provenance` — a single `npm publish` (v2 is not an npm workspace, so there is no v1-style `publish-all`/`--workspaces`), with a signed provenance attestation via GitHub OIDC (`id-token: write`, `environment: release`). Authentication is npm **trusted publishing** — there is no `NPM_TOKEN` secret, and adding one would break the publish (an empty `NODE_AUTH_TOKEN` makes npm fail `ENEEDAUTH` before OIDC is attempted). The job upgrades the npm CLI to `>=11.5.1` first, since trusted publishing requires it and Node 22 bundles npm 10.x.
- **`publish`** — the npm package. Runs `npm run pack:verify` as the pre-publish gate, asserts the release tag matches the root `package.json` version, then `npm publish --access public --provenance` — a single `npm publish` (v2 is not an npm workspace, so there is no v1-style `publish-all`/`--workspaces`), with a signed provenance attestation via GitHub OIDC (`id-token: write`, `environment: release`, `NPM_TOKEN`).
- **`publish-github-container-registry`** — the container image (see [Docker](#docker)).
A v2 release is cut from **`main`**, after the milestone's work has been merged there from `v2/main` — not from `v2/main` itself. (The v1 line releases independently from `v1/main` to the `v1-latest` tag and never touches `main`; see [Repo status](#mcp-inspector).)
Because there is **one version number** (only the root `package.json` has one — the clients carry none, so there is nothing to keep in sync and no `check-version` step), the release flow is just:
```bash

View file

@ -38,7 +38,7 @@ Please **do not** report security vulnerabilities through public GitHub issues,
or pull requests.
Note that this repository does not accept pull requests from outside contributors
(see [CONTRIBUTORS.md](./CONTRIBUTORS.md)) — **this does not apply to security
(see [CONTRIBUTING.md](./CONTRIBUTING.md)) — **this does not apply to security
reports**, which should always go through the advisory process above rather than
any public channel.

File diff suppressed because it is too large Load diff

View file

@ -46,16 +46,16 @@
"zod": "^4.3.6"
},
"devDependencies": {
"@eslint/js": "^9.39.4",
"@eslint/js": "^10.0.1",
"@types/express": "^5.0.6",
"@types/node": "^24.12.4",
"@vitest/coverage-v8": "^4.1.0",
"eslint": "^9.39.4",
"@vitest/coverage-v8": "^4.1.10",
"eslint": "^10.8.0",
"globals": "^17.4.0",
"prettier": "3.8.4",
"tsup": "^8.5.0",
"typescript": "~5.9.3",
"typescript-eslint": "^8.56.1",
"vitest": "^4.1.0"
"vitest": "^4.1.10"
}
}

View file

@ -535,7 +535,7 @@ async function parseArgs(argv?: string[]): Promise<ParseResult> {
const scriptArgs = rawArgs.slice(2);
const dashDashIndex = scriptArgs.indexOf("--");
let targetArgs: string[] = [];
let optionArgs: string[] = [];
let optionArgs: string[];
if (dashDashIndex >= 0) {
targetArgs = scriptArgs.slice(0, dashDashIndex);
optionArgs = scriptArgs.slice(dashDashIndex + 1);
@ -977,7 +977,8 @@ async function parseArgs(argv?: string[]): Promise<ParseResult> {
parsed = JSON.parse(options.toolArgsJson);
} catch (e) {
throw new Error(
`--tool-args-json is not valid JSON: ${(e as Error).message}`,
`--tool-args-json is not valid JSON: ${e instanceof Error ? e.message : String(e)}`,
{ cause: e },
);
}
if (

View file

@ -301,6 +301,7 @@ export async function runMethod(
} catch (e) {
throw new Error(
`--roots-json is invalid: ${e instanceof Error ? e.message : String(e)}`,
{ cause: e },
);
}
await inspectorClient.setRoots(roots);

File diff suppressed because it is too large Load diff

View file

@ -26,14 +26,14 @@
"commander": "^13.1.0"
},
"devDependencies": {
"@eslint/js": "^9.39.4",
"@eslint/js": "^10.0.1",
"@types/node": "^24.12.4",
"@vitest/coverage-v8": "^4.1.0",
"eslint": "^9.39.4",
"@vitest/coverage-v8": "^4.1.10",
"eslint": "^10.8.0",
"globals": "^17.4.0",
"prettier": "3.8.4",
"typescript": "~5.9.3",
"typescript-eslint": "^8.56.1",
"vitest": "^4.1.0"
"vitest": "^4.1.10"
}
}

Some files were not shown because too many files have changed in this diff Show more