Migrate all three clients + core + test-servers from
@modelcontextprotocol/sdk@1.29 to SDK v2 (2.0.0-beta.4), keeping behavior
identical against legacy (2025-11-25) servers. Foundation for the SDK V2 +
New Spec stack.
Packages
- Split @modelcontextprotocol/sdk -> @modelcontextprotocol/client (Client,
transports, OAuth, error taxonomy, protocol types) + core (Zod schemas);
stdio -> client/stdio, Ajv -> client/validators/ajv. Test-servers ->
@modelcontextprotocol/server (+ server-legacy/sse). Deterministic import
rewrite (schemas -> core, everything else -> client); the codemod misroutes
client code to /server so it was not used for path routing.
- new Client(..., { versionNegotiation: { mode: 'legacy' } }) — nothing
2026-era goes on the wire.
Runtime (core/mcp/inspectorClient.ts)
- Error taxonomy: McpError -> ProtocolError, ErrorCode -> ProtocolErrorCode;
RequestTimeout -> SdkError/SdkErrorCode.
- Handlers take method strings; task methods (tasks/*, notifications/tasks/*)
use the 3-arg custom form (excluded from v2's spec-method set).
- Legacy tasks kept working (per project requirement): requestor tasks re-
implemented over raw client.request + explicit deprecated schemas;
experimental.tasks.callToolStream replaced by a local pollTaskToolCall
async generator (with _progressHandlers revival so task-execution progress
still reaches the caller). Receiver (server-initiated) tasks: the v2 Client
validates a sampling/elicit handler result and rejects the legacy { task }
response with -32602 — installReceiverTaskResponseBypass swaps the wrapped
entry in the private _requestHandlers map for the task-augmented branch so
{ task } reaches the wire (fixes server-initiated sampling-via-task).
- Pagination: list verbs use raw client.request({method}, List*ResultSchema)
for single-page behavior (v2's client.listX() auto-aggregate all pages).
Web / auth / apps
- Rename local JsonSchemaType -> InspectorFormSchema + add toFormSchema()
structural narrowing at the SDK-schema -> form boundary.
- ext-apps still peers on SDK v1: one compat boundary with documented casts
(remove when ext-apps#702 lands a v2 peer).
- OAuth provider saveClientInformation reconciled with v2's
OAuthClientInformationContext; InvalidClientMetadataError re-homed as a
local class (server-legacy/auth is not browser-safe).
- vitest.shared.mts dedupes @modelcontextprotocol/client+core to one copy so
vi.mock intercepts the SUT's import.
Accepted v2 behavior changes (per #1624 / spec §9.2): unknown-tool call
rejects with -32602; cancellation is stream-abort (no guaranteed
notifications/cancelled frame); output-schema strictness restored via the
Inspector's own Ajv check on the default path.
npm run ci green: validate (web 3087 / cli 212 / tui 274 / launcher 5),
coverage (>=90 per-file, all clients), smoke (launcher/cli/web), storybook
(410).
Closes#1624
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Wrap the Node transport's fetch with an undici EnvHttpProxyAgent dispatcher
when a standard proxy env var is set, so the CLI and web backend can reach
remote MCP servers through corporate proxies with zero new flags. undici is
imported lazily (only when a proxy var is present) and added as a dependency
at the repo root and in the CLI client; a missing undici raises an actionable
error. Documents the behavior and dependency in the CLI README.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S3fTN8H3R8YV4yUGvZjYnX
* feat(web,core): edit STDIO cwd/env in Server Settings (#1515)
Persist and edit a STDIO server's working directory and environment
variables through the Server Settings modal.
- Add `env` (pair-array, mirrors headers) and `cwd?` to
`InspectorServerSettings`.
- `storedFieldsToInspectorSettings` lifts the stdio config's `env`/`cwd`
into settings so the modal can show them; they stay on `config` for the
transport. Add `envRecordToPairs` / `envPairsToRecord` helpers.
- `env`/`cwd` remain config-owned on disk (the standard mcp.json location):
the `/api/servers` PUT route writes an edited settings.env/cwd back onto
the stored stdio config on a settings-only patch (supporting clear), and a
config-providing PUT keeps config authoritative. `validateSettings`
accepts `env`/`cwd`.
- Server Settings modal: Working Directory field in Options and a dedicated
collapsible Environment Variables section — both STDIO-only.
- Registry server.json import sets no `cwd` (the schema has no
working-directory concept); documented in serverJson.ts.
Tests: serverList converter round-trip, route write-through (set/clear,
config-precedence, non-stdio no-leak, validation), import preservation
(client-config cwd, registry cwd skip), and form/modal component coverage.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(web): preserve config env/cwd when a settings PUT omits them (#1515 review)
Address Claude's review of #1518.
- `/api/servers` PUT now distinguishes an *absent* `env`/`cwd` (preserve the
stored value) from an explicit empty list / blank string (clear). Previously
`validateSettings` coerced a missing `env` to `[]`, so an env-unaware caller
patching only a timeout would silently wipe the server's env/cwd. The
integrated web client always resends the full GET-rehydrated env, so it was
unaffected — this protects the raw HTTP contract.
- `validateSettings` reports `envProvided`/`cwdProvided`; the write-through only
touches `config.env`/`cwd` for fields the caller actually sent.
- On the omit path, rehydrate the untouched env's keychain values onto the
rebuilt entry so the secret reconcile re-persists them rather than sweeping
the blanked-on-disk keys out of the keychain.
- Minor: guard the stdio cast in `mcpConfigToServerEntries` on the transport
type instead of casting unconditionally.
- Test: settings-only PUT omitting env/cwd preserves config keys + cwd + the
keychain secret.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(core): trace dropped fetch body update when entry rotates out (#1390)
FetchRequestLogState.onFetchRequestBodyUpdate silently dropped a deferred
response-body update when the matching entry had already been evicted by
log rotation (>= maxFetchRequests newer requests appended between the
append and the fire-and-forget body read). The user saw an empty Response
Body with no way to distinguish "body never came back" from "body dropped
after eviction".
Plumb an optional pino logger into FetchRequestLogState (defaulting to the
silent logger) and emit a logger.debug at the early-return site so the drop
is traceable. The web client wires the existing remote logger from
createWebEnvironment into the state manager.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(core): emit dropped-body trace at warn level so it is observable (#1390)
Review feedback: the web remote logger (createRemoteLogger) defaults to
level 'info' with no override at the environmentFactory call site, so a
logger.debug at the drop site is filtered out by both the pino level gate
and transmit.level — the diagnostic never reaches /api/log and the drop
stays invisible in the shipped web app, defeating the issue's goal.
Promote the trace to logger.warn (>= info, so it transmits by default). A
response body lost to log rotation is unexpected data loss, not routine
diagnostic noise, so warn is the appropriate severity. Update the comment
to explain the level choice and switch the test stub from debug to warn,
extracting a small typed makeWarnLogger helper per review.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(web): configurable Network Log Size per server + body-dropped toast (#1390)
Turn the otherwise-invisible "response body dropped after rotation" case into
an actionable signal:
1. Per-server `maxFetchRequests` setting (default 1000, 0 = unlimited),
editable as "Network Log Size" in the first (Options) accordion section of
the Server Settings form. Plumbed through the full settings pipeline:
InspectorServerSettings + StoredMCPServer types, the serverList disk
converters (read defaults to 1000, write omits the default to keep diffs
minimal — 0 is a meaningful non-default), and the node server's GET-sanitize
+ write-side validateSettings (non-negative number). Applied live to the
active connection on settings-modal close via a new
FetchRequestLogState.setMaxFetchRequests (shrinking trims immediately), and
at connect time for the next session.
2. FetchRequestLogState now emits a `fetchRequestBodyDropped` event, but only
for genuine rotation drops (log at capacity with a finite cap) — not for
benign post-clear stragglers or unlimited logs, which previously also hit
the warn path. App shows a deduped, persistent toast (stable per-server id,
no auto-close) explaining what happened and the likely causes, with a link
that opens this server's settings on the Options section (modal remounts via
key) so the user can raise the limit if it's just a high-traffic server.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(web): don't treat a cleared Network Log Size field as unlimited (#1390)
Review feedback: clearing the NumberInput emits "", and `parseInt("") || 0`
coerced that to 0 (unlimited). Since the value applies live on modal close, a
user who cleared the field to retype and then closed the modal would silently
switch the log to unlimited. Fall back to the current value on an empty/NaN
parse and reserve 0 for an explicit numeric entry. Adds a cleared-input test.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(web): add per-server Roots config + client roots capability (#1425)
Add MCP roots support to the v2 web Inspector as a per-server config
section in the server settings form (no separate Roots tab/screen).
- Persistence: add `roots: Root[]` to `InspectorServerSettings` and
`StoredMCPServer`; thread through serverList converters (lift on read,
drop empty-uri rows / blank names on write, omit when empty) and the
remote server's read-side drop guard + `validateSettings`.
- Capability: `setupClientForServer` always advertises the roots
capability (`roots: cleanRoots(savedSettings?.roots ?? [])`) so the
server can issue `roots/list` and receive `roots/list_changed`.
- Live apply: edits to a connected server's roots are pushed via
`inspectorClient.setRoots(...)` once, on settings-dialog close — diffed
against the client's current roots — rather than per keystroke, to
avoid flooding the server with `roots/list_changed` / `roots/list`.
- Form: new Roots accordion section with a required URI and an optional
name per row (unlike v1, which was URI-only), + Add Root, and remove.
Tests cover the persistence round-trip (serverList + servers-route),
the form/modal section, and reuse the existing InspectorClient
roots/`setRoots` integration coverage.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(roots): address review — dedup guards/cleanRoots, preserve extra fields, test live-apply
Responding to the PR #1431 code review:
- Dedup `isRootArray`: drop the copy inside `validateSettings` and reuse the
module-scope guard the read path uses, so the two can't drift
(core/mcp/remote/node/server.ts).
- Dedup the clean-roots normalizer: hoist a single exported `cleanRoots`
into core/mcp/serverList.ts and reuse it from both
`inspectorSettingsToStoredFields` and App.tsx (drops the local copy).
- Preserve non-form root fields (e.g. `_meta`): `cleanRoots` now spreads the
carried-through fields and only normalizes uri/name, and the modal's
`handleRootChange` spreads the existing root on edit — so a hand-edited
root's `_meta` survives load + persist + edit instead of being silently
dropped.
- Test the App live-apply-on-close path: new App.test.tsx cases assert
setRoots is called once with cleaned roots when changed on the active
server, and not called when the cleaned roots already match what the
client advertises. Adds a cleanRoots unit test (blank drop + _meta
preserve).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Wires the ported Tasks screen + core task subsystem through App.tsx: sticky Clear Completed, cancel/refresh error toasts, core-side per-task progress correlation, Run-as-task on the Tools screen (gated on server support), per-server Task TTL, and live task-status toasts.
* feat(servers): persist per-server settings to mcp.json (#1352)
- Add optional `settings` node alongside the transport config in each
`~/.mcp-inspector/mcp.json` entry. Round-trips through
`mcpConfigToServerEntries` / `serverEntriesToMcpConfig` and the
`/api/servers` POST/PUT routes.
- Drop `MCPServerConfig.headers` from `SseServerConfig` /
`StreamableHttpServerConfig` and remove the headers textarea from
`ServerConfigModal`. Custom headers are now entered only in
`ServerSettingsForm` and reach the wire via `settings.headers`.
- Wire `ServerSettingsModal` into `App.tsx`; saves dispatch through a new
`useServers.updateServerSettings` mutator. `updateServer` carries the
existing settings across config-only saves so they aren't dropped.
- Apply settings on the *first* outbound request after a server loads —
no need to open the settings form first:
- `settings.headers` → SSE / streamable-http wire headers.
- `settings.metadata` → `InspectorClientOptions.defaultMetadata`,
merged into every outgoing `_meta` (call-time keys win).
- `settings.requestTimeout` → `InspectorClientOptions.timeout`.
- `settings.connectionTimeout` → Promise.race wrapper around connect().
- `settings.oauth*` → pre-seeded OAuth client credentials.
- `RemoteConnectRequest` carries `settings` so the remote backend can
source transport headers from the same source as the local path.
- `oauthClientSecret` is persisted in `mcp.json` alongside stdio `env`
values, both protected by the file's `0o600` permission.
- Update `specification/v2_servers_file.md` with the new shape and the
removal of the legacy headers field.
Tests: round-trip in `serverList.test.ts`; persistence on POST/PUT in
`servers-route.test.ts`; first-connect header injection in
`transport.test.ts`; `defaultMetadata` merging in
`inspectorClient.test.ts`; `useServers.updateServerSettings` +
`updateServer` settings-preservation in `useServers.test.tsx`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(servers): rebuild InspectorClient on every connect so settings refresh
The previous onToggleConnection branch reused the existing InspectorClient
on a same-server reconnect, which froze settings at the moment the client
was first constructed. Editing headers, metadata, timeouts, or OAuth
credentials in the settings form was therefore invisible until the user
either switched servers or reloaded the page — surprising right after a
visible save.
Always call setupClientForServer on (re)connect so the latest
target.settings flow through to the transport's headers / requestInit, to
defaultMetadata, and to the OAuth manager.
Also adds remoteClientTransport tests that pin the wire-level contract:
settings round-trip through the /api/mcp/connect body when supplied, and
the field is omitted when no settings are configured.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(history): stop wiping pendingRequestEntries inside the connect event
State managers (ManagedToolsState, ManagedPromptsState, ManagedResourcesState,
ManagedResourceTemplatesState, ManagedRequestorTasksState) handle the
client's "connect" event by calling `void this.refresh()`. The synchronous
portion of refresh runs through SDK Protocol.request → transport.send →
MessageTrackingTransport.send → trackRequest, all before yielding to the
network — so each list request gets tracked into MessageLogState's
pendingRequestEntries inside the same connect event dispatch.
MessageLogState's own "connect" listener was registered after those state
managers (it's the last one constructed in App.tsx) and cleared
pendingRequestEntries on every connect. Listener order meant the clear ran
*after* the requests were tracked but before their responses arrived,
leaving the history with unmatched "response" entries marked PENDING.
The clear-on-disconnect (via statusChange → disconnected) already handles
session-boundary cleanup, so clear-on-connect was purely defensive and now
actively harmful. Drop the connect listener; keep the disconnect path.
Tests: updated the unit test to assert the new behavior (connect does not
clear); added an integration regression that wires a sibling connect
listener firing listTools and asserts the response folds into the request
entry instead of appearing as an orphan.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore: drop accidentally tracked debugging screenshot
pending-requests.png was a local debugging artifact attached to a
conversation, not source. Remove it from the tree (the working copy is
kept so the user still has it locally) and rely on the existing untracked
behavior going forward.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test(servers): add e2e check that settings.headers reach upstream MCP
Stitches the full v2 pipeline together in one assertion:
browser-style RemoteClientTransport
→ POST /api/mcp/connect (settings in body)
→ backend createTransportNode applies settings.headers to requestInit
→ SDK StreamableHTTPClientTransport
→ upstream MCP test server
Captures the upstream HTTP request via FetchRequestLogState and asserts
both `X-Tenant` and `X-Trace` are present on the outbound POST. Catches
any regression that breaks the wire between the form and the upstream
server in a single integration test, instead of the previous coverage
which exercised each hop in isolation.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(ServerSettingsModal): invert ListToggle compact prop so icon matches state
ListToggle's `compact` prop represents the list's *current* compact state:
true means "currently compact" and renders the expand icon to advertise
the action (open up). The settings modal was passing
`compact={allExpanded}` — exactly inverted. When all sections were open
it showed the expand icon (suggesting "open more") and when collapsed it
showed the collapse icon, opposite of every other ListToggle caller in
the app.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* style(HistoryListPanel): match ServerListScreen Export button — rename + default variant
Renames "Export JSON" to "Export" and switches from the subtle ToolbarButton
to the same `Button variant="default"` used by ServerListControls so the
two Export affordances look identical across screens. Also moves the
button to the right of the ListToggle (mirroring the order on the server
list screen) and disables it when there are no entries to export.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* style(HistoryListPanel): move Clear button to top toolbar, match Export style
Moves the Clear affordance out of the "History (N)" section header and
up to the panel toolbar alongside Export, where it now uses the same
`Button variant="default"` styling. Order in the toolbar is now:
ListToggle · Clear · Export. Clear is disabled when there are no
unpinned entries to remove (pinned entries are intentionally preserved).
ToolbarButton helper is unused after this change and dropped.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore: ignore local debugging screenshots at repo root
Stops `git add -A` from re-staging the `*.png` files Claude Code drops
at the repo root from attached conversation screenshots. The pattern
only matches the top-level (`/*.png`) so legitimate image assets in
subdirectories (e.g. `clients/web/public/`) still get tracked when
explicitly added.
Also untracks the two screenshots that slipped through previously
(initialize-request.png, pending-requests.png).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* style(LogStreamPanel): match HistoryListPanel button styling
Clear / Export / Copy All on the Logging screen were rendered with the
subtle ToolbarButton helper while History's equivalents use
`Button variant="default"`. Switch all three to the default variant so
the toolbars look identical across screens and mirror the disabling
behavior — each button is now disabled when `entries.length === 0`.
ToolbarButton helper is unused after this change and dropped.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(servers): address PR review — debounce save, preserve-on-omit PUT, validate shape, lift connect timeout
Round-up of fixes from https://github.com/modelcontextprotocol/inspector/pull/1353#issuecomment-4526861959:
1. ServerSettingsModal saves were firing on every keystroke (one PUT +
full mcp.json refresh per character). App.tsx now debounces by 300ms
and flushes on modal close so a burst of edits coalesces into one PUT
and the form value can't flicker back mid-typing.
2. `PUT /api/servers/:id` with an omitted `settings` field used to
silently wipe the persisted settings node. The route now treats the
three intents distinctly:
- field omitted → preserve existing on-disk settings
- explicit null → clear the settings node
- settings object → validate + apply
`useServers.updateServer` is simplified to drop the client-side
carry-through hack, and the test that documented the broken behavior
is flipped + augmented with explicit-clear and bad-shape cases.
3. `connectionTimeout` Promise.race is moved from App.tsx into
`InspectorClient.connect()`, reading `this.serverSettings?.connectionTimeout`.
TUI/CLI consumers now get the same behavior; on timeout the client
internally disconnects so the next connect starts clean.
4. Custom Headers hint in `ServerSettingsForm` notes that `Authorization`
is owned by the OAuth flow when OAuth is configured — avoids the
sharp edge where the SDK's authProvider silently overwrites a
user-set Authorization header.
5. New `validateSettings` on the server route rejects malformed shapes
(`settings: []`, `settings.headers: "oops"`, non-numeric timeouts,
etc.) with 400 instead of letting them persist and crash the UI.
7 (nit). Renamed `settings` → `savedSettings` inside `setupClientForServer`
so the variable holding the persisted settings doesn't shadow the
later locally-derived values.
Follow-ups opened separately:
#1356 — Move oauthClientSecret out of mcp.json into the OS keychain (review #6).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(servers): address PR review pass 2 — connect-timeout test, unmount cleanup, tagged intent, lenient settings read
Pass-2 review at https://github.com/modelcontextprotocol/inspector/pull/1353#issuecomment-4527089705:
(a) New integration test pins the new client-layer connect-timeout
contract: a hanging stub transport with `connectionTimeout: 50`
rejects with the descriptive error and lands the client in "error"
status. Documents what TUI/CLI consumers should expect from the
lifted Promise.race in InspectorClient.connect().
(b) useEffect cleanup clears `pendingSettingsTimerRef` on unmount so a
route change or HMR mid-debounce can't fire one stale PUT against
an unmounted component.
(c) Refactor the PUT-route `settingsIntent` discriminator from
`"preserve" | "clear" | { value }` to a tagged union with `kind`,
and consume it with a `switch` for exhaustiveness — reads cleaner
and makes adding a fourth intent painless.
(d) `normalizeMcpServers` now runs `validateSettings` on the on-disk
settings node and silently strips it if invalid, mirroring the
existing lenient-on-read pattern from `normalizeServerType`. Closes
the read/write asymmetry where a hand-edited malformed `settings`
could survive on disk and get carried through preserve-on-PUT.
New route test pins the strip-on-GET + no-propagate-on-preserve-PUT
behavior.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(servers): pass-3 review — patch-style PUT, toast on close-flush fail
Pass-3 review at https://github.com/modelcontextprotocol/inspector/pull/1353#issuecomment-4529276897.
(b) `PUT /api/servers/:id` now treats both `config` and `settings` as
optional patches:
- field omitted → preserve the on-disk value
- explicit body value → apply
- settings: null → clear
`updateServerSettings` stops reading `existing.config` from the
in-memory `servers` snapshot, which closes the closure-capture
hazard where a refresh between debounce-schedule and flush could
silently revert a separate edit. The route is the single source of
truth for transport config now. New tests cover settings-only PUT,
rejected non-object config, and the empty-body no-op patch case.
The previous "rejects a missing config" test is rewritten to
document the new no-op-preserve semantics.
(c) Inline comment near the PUT rebuild loop documents that writing
the full mcpServers map back is what gives the route its desirable
self-healing property for malformed settings on *other* entries —
deliberate side-effect, not an accident of normalization.
(d) `flushPendingSettings` surfaces failures via @mantine/notifications
instead of swallowing them. The modal already closed by the time
the flush PUT resolves, so a silent fail would leave the user
thinking their last edits saved (especially painful for the OAuth
client secret). The notifications wiring was already mounted in
main.tsx.
Pass-3 (a) — settings form being effectively uncontrolled mid-edit — is
acknowledged in the reply: working as intended today; the proper fix is
internal modal state when file-watching (#1345) forces the issue.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(servers): pass-4 review — strip smuggled settings out of config
Pass-4 review at https://github.com/modelcontextprotocol/inspector/pull/1353#issuecomment-4529861349.
Main finding: `normalizeServerType` spreads unknown keys, so a body
that nested `settings` inside `config` would smuggle a settings field
onto the stored entry without ever passing through `validateSettings`.
Strip the settings key off the incoming config in `buildStoredEntry`
so `validateSettings` remains the single write path for the settings
node — the "one source of truth" invariant pass 2 set up holds again.
Two new tests pin the strip on both routes:
- POST with `config.settings = { bogus }` lands an entry with no
settings node on disk.
- PUT with `config.settings = { bogus }` + `settings: null` clears
the real settings *and* doesn't re-attach the bogus payload via
the spread.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(servers): pass-5 review — absorb late connect rejection, tighten validateSettings, log smuggle warning
Pass-5 review at https://github.com/modelcontextprotocol/inspector/pull/1353#issuecomment-4529954490.
Main finding: when the connect-time `Promise.race` lost to the timeout,
the original `this.client.connect(this.transport)` promise was left
pending. After `disconnect()` tore the transport down, real transports
(SSE / streamable-http) would reject that promise later — with no
handler attached, producing a Node `unhandledRejection` (and a vitest
suite warning). Attach a no-op `.catch` right after creating the
connect promise so Node sees the late rejection as handled.
Nit (a): `validateSettings` was casting the raw object through to
`InspectorServerSettings`, which let unknown keys ride along onto disk.
Replaced the cast with an explicit pick-and-build over the known
fields. Unknown stowaways now silently drop on the way through. New
route test pins the behavior.
Nit (b): `buildStoredEntry` now logs a `warn` via the route's pino
logger when an incoming `config` carries a `settings` key. The strip
is still defensive (it isn't a documented contract) but the warning
lets a misconfigured client find out about the wasted field instead
of debugging a silent drop.
Nit (c) — the defensive `?? {}` fallback in `buildStoredEntry` —
acknowledged in the review reply; left in place as belt-and-braces.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(servers): pass-6 polish — scope absorber, thread id, drop empty OAuth, guard `in`
Pass-6 review at https://github.com/modelcontextprotocol/inspector/pull/1353#issuecomment-4529979135. Reviewer signed off as ready; landing the four marginal cleanups since they're all small.
(a) `connectPromise.catch(() => {})` moves inside the
`if (connectTimeoutMs > 0)` branch. The await-only path propagates
rejection cleanly through the outer try/catch — the absorber is
only needed when the race leaves the original promise unattended.
Code now reads as "absorber is tied to the race," which is the
real invariant.
(b) Thread `id` (POST id, PUT newId) through `buildStoredEntry` and
include it in the smuggle warning's pino bindings. Two clients
smuggling simultaneously are distinguishable in the log instead
of indistinguishable.
(c) `validateSettings` coerces empty-string OAuth fields to absent.
The form emits `""` when the user clears an input, and an empty
`oauthClientId` on disk would later be misread as "OAuth configured."
New route test pins it.
(d) `buildStoredEntry` adds a null/object guard before `"settings" in
configObj`. The route layer pre-checks this on POST and PUT, but
the helper's `unknown` parameter is a weak contract; the guard
makes it safe to call from anywhere.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(spec): reflect settings persistence + patch-PUT contract in v2_servers_file
The doc was authored when `mcp.json` only stored the server list. Bring it
forward to match what shipped in #1352 / PR #1353:
- Summary calls out that the file also stores per-server settings and
links into the Per-server settings section.
- On-disk format note explains that `settings` is an optional Inspector-
specific extension that other MCP tools ignore.
- New top paragraph on the Per-server settings section explains the
basic-config / settings UI split rationale (simpler form for the
common case, advanced settings tucked behind a second dialog).
- UI bullet is rewritten to match the patch-style PUT semantics that
emerged across the review cycle: settings-only and config-only
PUTs preserve their counterpart inside the write lock, so neither
modal can silently clobber the other half. The earlier "must
re-send settings on PUT" hack is gone.
- New bullets capture the save cadence (300ms debounce + flush-on-close
+ failure toast), the patch-PUT contract (omit-preserve / null-clear /
rename), and the write-path / read-path validator gates.
- Secret storage bullet links to #1356 (OS-keychain follow-up).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(spec): show a third entry with the optional settings node
The On-disk format example demonstrated only basic stdio entries. Add a
streamable-http entry with a populated `settings` node so readers can
see the shape inline rather than having to scroll to the Per-server
settings section. Inline comment in the block cross-links there for the
full contract and notes that other MCP tools ignore the extension key.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test: switch integration project to folder-based manifest (#1314)
Replace the `integrationTests` enumeration in vite.config.ts with a
folder glob (`src/test/integration/**/*.test.ts`). Files under that
folder run in the integration vitest project (node env, 30s timeouts);
everything else runs in the unit project (happy-dom).
This removes the silent misclassification trap from #1307: a new
integration-style test no longer falls into the unit project (and
happy-dom) just because someone forgot to add it to the array.
The 13 ported v1.5 tests + their helpers move under src/test/integration/
in a strict mirror of core/ source layout (mcp/, auth/, auth/node/,
storage/). Renames use git mv so history is preserved.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test: address PR review on #1318 (glob extensions + AGENTS doc)
- vite.config.ts: widen integrationGlob from `*.test.ts` to
`*.test.{ts,tsx}` so the integration folder fully owns its contents.
Without this, a `.test.tsx` placed under src/test/integration/ would
match the unit project's `{ts,tsx}` include, fail the unit `.ts`-only
exclude, and silently land under happy-dom — the exact misclassification
trap this PR set out to close.
- AGENTS.md: update the line describing where integration tests live;
it still pointed at the old `src/test/core/` location and the removed
`integrationTests` enumeration.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>