What agents should know before contributing
modelcontextprotocol/inspector
Visual testing tool for MCP servers
Read by Bluet from 9 files written for agents and 300 merged pull requests on 2026-09-24. Not reviewed by the maintainers.
Rules its files tell agents (22)
- docs/environment-variables.md
mustNever set it in a shell profile, a shared environment, or a deployment.
⚠️ This disables certificate verification for every TLS connection the Inspector process makes — every MCP server, every OAuth authorization server and token endpoint — not just the one you are testing, and Node prints a warning saying so. A connection that would have been refused for impersonation is then accepted. Never set it in a shell profile, a shared environment, or a deployment.
- docs/docker.md
mustThe Inspector answers the request with a real loopback origin on a third listener, MCP_APP_ORIGIN_PORT (defaul…
And 6278 if your app declares _meta.ui.domain. That is the spec field a server uses to ask its host for a stable, dedicated origin — without one the app runs at an opaque origin and its requests carry Origin: null, which no CORS / OAuth-callback / API-key allowlist can admit. The Inspector answers the request with a real loopback origin on a third listener, MCP_APP_ORIGIN_PORT (default 6278); apps that declare no domain never touch it. Publish it if you use one — this is the one failure that does not fall back: the backend publishes fine (its listener bound inside the container), so it hands t
- docs/cli-smoke-testing.md
mustThe flag is a no-op against a server that never challenges, so a green run is not evidence that your token…
⚠️ The flag is a no-op against a server that never challenges, so a green run is not evidence that your token store was seeded correctly. A smoke job whose server authenticates today and stops authenticating tomorrow — a misconfigured gateway, a route that silently became public — will keep passing. If you need to assert that authentication actually happened, assert it directly: run once without a usable token in an isolated MCP_STORAGE_DIR and require exit 3.
- docs/cli-smoke-testing.md
mustNever merge them (2>&1) into something you then pipe to jq — the one place this guide does merge them is the s…
stdout is the result; stderr is diagnostics. Never merge them (2>&1) into something you then pipe to jq — the one place this guide does merge them is the secret scan in §7, which greps rather than parses.
- docs/cli-smoke-testing.md
mustA CI job should never inherit a disabled timeout — a black-holed host would hang the runner until the job's ow…
Always bound the connect. --connect-timeout <ms> defaults to 15000 for ad-hoc --server-url/target runs and to the file-level timeout for --catalog/--config runs; 0 disables it. A CI job should never inherit a disabled timeout — a black-holed host would hang the runner until the job's own limit kills it.
- AGENTS.md
mustTargeting the highest would name a version three of them do not have and write a marker that suppresses the re…
The target is the LOWEST latest across a group — the version the whole group has reached — not the highest. npm publishes a lockstep release one package at a time, so a sweep landing mid-publish sees one package ahead of its three siblings. Targeting the highest would name a version three of them do not have and write a marker that suppresses the real filing once the publication completes, so the release would never be tracked at all. Taking the minimum keeps the issue actionable and lets the completed release file its own.
- AGENTS.md
mustNever raise the tsc heap to work around one.
One version per install-crossing dependency. When bumping a dependency the shared sources pull in, bump it in every install that declares it. Consolidating to the root is what makes most of these unbumpable in two places at once, but it does not retire the rule — a client's devDependencies, and any package that arrives transitively into a client install, can still skew against the root. Never raise the tsc heap to work around one. npm run verify:dep-lockstep enforces this in two tiers: packages that reach one tsc program from two installs (the #1896 heap-exhaustion class), and — since #2226 —
- AGENTS.md
mustTwo limits remain: the tier reads lockfiles, so a tool binary you installed by hand and never committed is sti…
⚠️ Deleting the declaration does not always delete the copy, and the local copy still wins. npm auto-installs an unmet peer into the install that needs it, and it has no visibility into the root's tree — so a client-only ESLint plugin drags a client-local eslint in (eslint-plugin-react-refresh/-storybook in web, eslint-plugin-react-hooks in tui), and web's Storybook/Vitest stack drags in a local typescript and vitest. A hoisted transitive does the same: @types/express puts an @types/node in web and cli. Those copies sit nearer than the root's and take precedence. The consolidation is therefore
- AGENTS.md
mustRe-adding a root-declared package to a client manifest re-creates the second copy this rule exists to make imp…
A client declares only what that client alone consumes — its own UI stack, its bundler-inlined packages, its dev tooling. clients/cli and clients/launcher therefore declare no runtime dependencies at all, and that is the expected steady state, not an omission: everything they run on is root-declared and resolves by walk-up from the client directory. Re-adding a root-declared package to a client manifest re-creates the second copy this rule exists to make impossible (#1896), so a missing module at runtime is a signal to check the root manifest and the client's external list, never to add it bac
- docs/environment-variables.md
shouldUnset a variable rather than setting it to an empty string.
⚠️ Unset a variable rather than setting it to an empty string. The two are not interchangeable: HOST="" is read as an all-interfaces bind and refused, and an empty path variable such as MCP_STORAGE_DIR= or MCP_INSPECTOR_LOG_DIR= can resolve relative to the working directory instead of falling back to the default. A row says so explicitly where an empty value is treated as unset.
- docs/docker.md
shouldKeep supplying the same passphrase on every run: a file that can no longer be decrypted is read as empty an…
⚠️ Keep supplying the same passphrase on every run: a file that can no longer be decrypted is read as empty and refuses to be written. Everything else about the store — the selection order, the file's location, encryption, permissions, locking, and choosing a store explicitly with MCP_INSPECTOR_SECRET_STORE — applies to every runtime and is in Where secrets are stored.
- docs/docker.md
shouldDon't loosen the mode to make it readable instead: that hands the key to every other user on the host, and any…
[!WARNING] Mounting that volume turns on file storage of secrets, and without a key the file is plaintext. Every OAuth client secret, IdP client secret and stdio env: value you save is then written to secrets.json on the volume, readable by anyone who can read the volume: root and every member of the docker group on the host, and anyone who gets a backup, snapshot or copy of it. Mode 0600 only keeps out other non-root users. > Give it a key, and keep that key only where the Inspector can read it. Generate one into a file only you can read, outside the volume, backups and any repository tha
- docs/docker.md
shouldDon't bind-mount the file on its own: it is not recognized as durable, so you get the memory store, and even w…
So the same volume that keeps your server list also switches secrets from session-scoped to durable — nothing extra to configure. The in-memory default for an unmounted container is deliberate: a file in the writable layer is discarded by --rm and by every image update, and promising durability it can't deliver is worse than declining to. The check looks at the directory that holds the secrets file, so if you relocate it with -e MCP_STORAGE_DIR=… or -e MCP_INSPECTOR_SECRET_FILE=…, mount a volume at that file's parent directory. Don't bind-mount the file on its own: it is not recognized as dura
- docs/cli-smoke-testing.md
should⚠️ set -e does not fire for a command on the left of |; only the pipeline's last status is checked unless set…
⚠️ set -e does not fire for a command on the left of |; only the pipeline's last status is checked unless set -o pipefail is also on. Every example here pipes into jq, so keep pipefail.
- docs/cli-smoke-testing.md
should⚠️ pipefail gives you a failure, not the CLI's failure.
⚠️ pipefail gives you a failure, not the CLI's failure. It reports the rightmost non-zero status, so when both sides fail the CLI's class is lost:
- docs/cli-smoke-testing.md
shouldCapture the status with || status=$?, not with if !
⚠️ Capture the status with || status=$?, not with if ! cmd; then status=$?. ! inverts the pipeline's status, so $? inside that branch is 0 and every failure class looks identical.
- docs/cli-smoke-testing.md
shouldThat coercion is the trap: a zip code, an order number, or an ID that happens to be all digits arrives at the…
That coercion is the trap: a zip code, an order number, or an ID that happens to be all digits arrives at the server as a number through --tool-arg, and a schema expecting a string rejects it. Prefer --tool-args-json for anything typed — it says exactly what you mean. Then assert on the payload:
- docs/cli-smoke-testing.md
shouldDo not put the recovery in a || { … } group and stop there.
⚠️ Do not put the recovery in a || { … } group and stop there. The group's own status becomes the list's status, so a successful jq turns a failed run into a green one — the exact failure this guide warns about twice elsewhere. Capture into status, report, then exit with it.
- docs/cli-smoke-testing.md
shouldA consumer that requires .result will break on every --app-info run:
⚠️ --app-info is a different shape, not a variation on this one. It probes without invoking the tool, so there is no result to report and the envelope is {"appInfo": …} alone. A consumer that requires .result will break on every --app-info run:
- docs/cli-smoke-testing.md
shouldIf your stdio server takes flags of its own, you need a -- separator, and it splits the opposite way from t…
⚠️ If your stdio server takes flags of its own, you need a -- separator, and it splits the opposite way from the web and TUI clients. Under --cli, everything before -- is the target command and everything after is the Inspector's own options:
- CONTRIBUTING.md
shouldPlease don't send a diff or open a pull request.
Please don't send a diff or open a pull request. Instead, open an issue that includes:
- AGENTS.md
should⚠️ vitest, @vitest/coverage-v8 and web's @vitest/browser-playwright are pinned exactly, and move together.
⚠️ vitest, @vitest/coverage-v8 and web's @vitest/browser-playwright are pinned exactly, and move together. @vitest/browser-playwright declares an exact peer on vitest, so it — not the root range — decides which vitest web installs. Left to float, the root resolves a newer patch and web's tests then run on one vitest while loading a coverage provider built against another. Bumping means editing all three in one change, the same discipline the exact prettier pin (#1790) exists for. ⚠️ Editing the three is necessary but not sufficient — clients/web also carries a vitest overrides entry that has t
Decisions its pull requests settled (40)
- #1749Server Settings: disambiguate EMA OAuth credential fields
- #1809start OAuth on a modern/auto-era connect
- #1798register peer-request handlers before connect
- #1810explicit re-authorize when the OAuth callback state was lost
- #1814wait for the TUI to exit before removing its temp dir
- #1834derive the npm dist-tag from the version instead of defaulting to latest
- #1895await the pack:verify web child's exit before removing its work dir
- #1836publish via OIDC trusted publishing, not a non-existent NPM_TOKEN
- #1945construct AsyncEntry inside the try so keychain degradation engages
- #1934ship clients/web/static so the MCP Apps sandbox proxy loads
- #1946prime SSE streams so Firefox resolves the events fetch
- #1954surface list-fetch failures instead of swallowing them
- #1948load @napi-rs/keyring lazily so an unsupported platform degrades instead of crashing
- #1958settle the deliberately in-flight tool calls in the progress tests
- #1961key tool rows by source position so duplicate names filter correctly
- #1967open the modern listen stream whenever the filter is non-empty
- #1974publish on loopback by default; make a mounted state dir writable
- #1972keep the text being typed in a schema number field
- #1983bundle React-rendering deps so a consumer install can't split React
- #1987wait for the dead transport instead of sleeping 300ms
- #2009bind loopback by address and pin the sandbox port for containers
- #2016salvage a list result's valid entries when one is malformed
- #2014render nullable and union argument fields correctly in the web form and the TUI
- #2017gate the connect-time logging/setLevel on the negotiated era
- #2024gate submission on unsendable field text, and on required tool arguments
- #2022per-server custom OAuth authorization-request parameters
- #2027stop a schema default from reverting the edit in progress
- #2032read modern tasks support from the extension map
- #2035expand resource templates per RFC 6570
- #2037Auth URL and Token URL overrides in Server Settings
- #2036accept CRLF-delimited SSE frames on the servers events stream
- #2047surface list-fetch failures in paginated mode
- #2045register SSE abort cleanup before the priming write
- #2050handle background command rejections in App
- #2083negotiated app-rendered form elicitations
- #2076file-backed and in-memory SecretStore for hosts with no keychain
- #2088give the secrets file a real cross-process lock
- #2091roll a failed settings write back to what actually landed
- #2094allow complex JSON values in request _meta
- #2098read MCP App UI resource metadata from _meta.ui