Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
55e25b7
fix(core): register peer-request handlers before connect (#1797)
cliffhall Jul 26, 2026
1d2d44b
review: round-1 fixes — pre-connect roots/list_changed, wider timing …
cliffhall Jul 27, 2026
8016857
review: round-2 fixes — correct the roots/list_changed premise (#1797)
cliffhall Jul 27, 2026
dd26289
review: round-3 fix — drop stale comments inside the roots handler (#…
cliffhall Jul 27, 2026
0a38fe6
fix(cli): advertise roots so roots/set is actually serviceable (#1797)
cliffhall Jul 27, 2026
b952142
fix(cli,tui): advertise the roots configured in mcp.json (#1797)
cliffhall Jul 27, 2026
b8e02e1
review: round-6 fixes — document roots in the CLI README, harden clea…
cliffhall Jul 27, 2026
866c2b5
review: round-7 fix — cleanRoots also throws on a non-string name (#1…
cliffhall Jul 27, 2026
344198b
fix(core): normalize roots in setRoots() too (#1797)
cliffhall Jul 27, 2026
5758de8
review: round-9 fixes — normalize roots in the constructor too (#1797)
cliffhall Jul 27, 2026
758b897
review: round-10 fixes — copy at both rootsChange dispatches (#1797)
cliffhall Jul 27, 2026
5482cb8
Merge remote-tracking branch 'origin/v2/main' into v2/fix-roots-handl…
cliffhall Jul 27, 2026
afccdbc
review: round-11 doc fixes; merge v2/main (#1797)
cliffhall Jul 27, 2026
7481695
fix(core): clear queued peer requests when connect() fails (#1797)
cliffhall Jul 27, 2026
4a717b0
fix(core): clear the peer-request queue on a mid-session crash too (#…
cliffhall Jul 27, 2026
ecd9c7d
review: round-14 fixes — make both teardown paths agree (#1797)
cliffhall Jul 27, 2026
7f83e9c
review: round-15 fixes — scope the parity claim to the arrays (#1797)
cliffhall Jul 27, 2026
166c64a
test: pin disconnect()'s clear-before-announce ordering (#1797)
cliffhall Jul 27, 2026
6c5a168
test: pin disconnect()'s announce as well as its clear (#1797)
cliffhall Jul 27, 2026
b079222
review: round-18 doc fixes — scope the three-path claim, re-attribute…
cliffhall Jul 27, 2026
77912b7
fix(core): gate roots/list on what was advertised, not on this.roots …
cliffhall Jul 27, 2026
402e473
review: round-20 — derive the roots gate from the advertisement itsel…
cliffhall Jul 27, 2026
2115a7c
fix(core): gate elicitation/create on what was advertised too (#1797)
cliffhall Jul 27, 2026
bdf65dc
fix(core): derive tasks.requests and the remaining gates from the adv…
cliffhall Jul 27, 2026
a54a16e
review: round-23 — pin the omitted-requests arm, drop the count again…
cliffhall Jul 27, 2026
8c2cdd9
fix(core): settle queued samples on teardown instead of discarding th…
cliffhall Jul 27, 2026
714f6fe
fix(core): mark receiver-task payload promises handled (#1797)
cliffhall Jul 27, 2026
e2621ae
review: round-26 — drop the comment that outlived its call (#1797)
cliffhall Jul 27, 2026
aba9919
review: round-27 — skip the roots/list_changed the SDK would refuse (…
cliffhall Jul 27, 2026
769e4a1
review: round-28 — gate the notification on the predicate the SDK ass…
cliffhall Jul 27, 2026
f0f0723
review: round-29 — chase the corrected premise into clients/cli (#1797)
cliffhall Jul 27, 2026
5700efc
fix(core): reject in-flight raw-wire requests on a mid-session crash …
cliffhall Jul 27, 2026
d104436
review: round-31 test fixes — justify the cast, drop the no-op tick (…
cliffhall Jul 27, 2026
e92ebc6
review: round-32 — separate the outbound teardown claim from the inbo…
cliffhall Jul 27, 2026
64bf2b9
review: round-33 — extract the timeout race, mark the failure-path pr…
cliffhall Jul 27, 2026
271f782
review: round-34 — drop a no-op guard and the false mechanism it asse…
cliffhall Jul 27, 2026
3301e92
review: round-35 — the drain comment described a trade-off nobody mad…
cliffhall Jul 27, 2026
42d511f
review: round-36 — the race comment outlived the mechanism its own co…
cliffhall Jul 27, 2026
73f26c7
review: round-37 — scope the timeout comment, align the bypass guards…
cliffhall Jul 27, 2026
e8ddf41
review: round-38 — one predicate for the whole bypass mechanism (#1797)
cliffhall Jul 27, 2026
66c854c
fix(core): don't carry receiver tasks into the next session (#1797)
cliffhall Jul 27, 2026
97ad44e
fix(core): reset all session-scoped state on connect, not just receiv…
cliffhall Jul 27, 2026
dc738a9
fix(web): re-seed the Logs control when the client re-seeds its own l…
cliffhall Jul 27, 2026
4dd1388
refactor(core): one derivation for the modern log level (#1797)
cliffhall Jul 27, 2026
9007740
fix(core): reset the subscription stream state with the set it derive…
cliffhall Jul 27, 2026
388de8f
fix(web,core): correct the re-seed branch; share the subscription-str…
cliffhall Jul 27, 2026
2a56323
review: round-45 — reattach the orphaned doc, order the announcements…
cliffhall Jul 27, 2026
303c392
fix(core): close the listen stream the connect-path reset drops (#1797)
cliffhall Jul 27, 2026
41c2db4
review: round-47 — fix the doc's count, catch the closed-promise chai…
cliffhall Jul 27, 2026
4669e87
review: round-48 — scope the catch, guard the close, index the test (…
cliffhall Jul 27, 2026
3e7ac45
review: round-49 — pin the best-effort close, unorphan the doc (#1797)
cliffhall Jul 27, 2026
69b0a0c
review: round-50 — sweep the queues start-clean, unify the close (#1797)
cliffhall Jul 27, 2026
6cf9ae0
review: round-51 — pin the sweep's ordering, name the category (#1797)
cliffhall Jul 27, 2026
aba2828
review: round-52 — say what the sweep prevents, scope the close doc (…
cliffhall Jul 27, 2026
904cdcf
review: round-53 — give each close site its real harm, pin it (#1797)
cliffhall Jul 27, 2026
88cbd1a
review: round-54 — the mildest site isn't mild from every caller (#1797)
cliffhall Jul 27, 2026
edb01fc
fix(core): don't roll back a subscribe a newer refresh superseded (#1…
cliffhall Jul 27, 2026
2a3e002
fix(core): reconcile the stream badge when a re-listen this call owne…
cliffhall Jul 27, 2026
e4b8692
fix(core): order the subscription writes, retry instead of stranding
cliffhall Jul 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion clients/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,9 @@ npx @modelcontextprotocol/inspector --cli https://my-mcp-server.example.com --tr
npx @modelcontextprotocol/inspector --cli https://my-mcp-server.example.com --transport http --method tools/list --header "X-API-Key: your-api-key"
```

When a server is loaded from a `--catalog`/`--config` file, its per-server settings (headers, connection/request timeouts, and OAuth) are applied to the connection — the same resolution the TUI uses. A `--header` flag overrides the file's headers for that run while leaving the file's timeouts and OAuth in place.
When a server is loaded from a `--catalog`/`--config` file, its per-server settings (headers, connection/request timeouts, OAuth, and `roots`) are applied to the connection — the same resolution the TUI uses. A `--header` flag overrides the file's headers for that run while leaving the file's timeouts and OAuth in place.

The file is the only durable way to give a run its roots: there is no roots flag, and `--method roots/set` applies only to that one short-lived connection. Roots configured for a server (the same field the web UI's Server Settings writes) are advertised at connect, so a server that asks for `roots/list` — `@modelcontextprotocol/server-filesystem` does, to learn its allowed directories — gets them.

**Environment-variable semantics.** `MCP_CATALOG_PATH` is honored only when no ad-hoc target is given (positional command, `--server-url`, or `--transport`) — so a shell that exports it can still run one-off ad-hoc invocations without hitting the catalog/ad-hoc conflict. `MCP_STORAGE_DIR` sets the storage directory used by the OAuth persist backend (`<MCP_STORAGE_DIR>/oauth.json`); the per-file `MCP_INSPECTOR_OAUTH_STATE_PATH` override still takes precedence over it.

Expand Down
84 changes: 84 additions & 0 deletions clients/cli/__tests__/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
getTestMcpServerCommand,
createTestServerHttp,
createEchoTool,
createListRootsTool,
createTestServerInfo,
} from "@modelcontextprotocol/inspector-test-server";
import type { MCPServerConfig } from "@modelcontextprotocol/inspector-core/mcp/index.js";
Expand Down Expand Up @@ -325,6 +326,89 @@ describe("CLI Tests", () => {
});
});

describe("Roots capability (#1797)", () => {
it("answers a server's roots/list instead of -32601", async () => {
// The CLI used to omit `roots` when constructing its InspectorClient, so
// the capability was never advertised and no `roots/list` handler was
// registered — a server that asked got -32601 Method not found. (And
// `--method roots/set` could not announce the change: the SDK refuses
// `roots/list_changed` from a client that never declared it, so nothing
// reached the wire.) `cli.ts` now always passes the `roots` option —
// empty on this ad-hoc path, since there is no config file.
const server = createTestServerHttp({
serverInfo: createTestServerInfo(),
tools: [createListRootsTool()],
});
try {
await server.start();

const result = await runCli([
server.url,
"--cli",
"--method",
"tools/call",
"--tool-name",
"list_roots",
]);

expectCliSuccess(result);
const json = expectValidJson(result);
// The tool asks the client for roots and renders what it got. An
// unregistered handler surfaces as an isError result quoting -32601.
expect(json.isError).toBeFalsy();
expect(JSON.stringify(json)).toContain("Roots:");
expect(JSON.stringify(json)).not.toContain("-32601");
} finally {
await server.stop();
}
});

it("answers with the roots configured for the server in the config file", async () => {
// Answering `roots/list` is only half the fix: the roots a user
// configured in mcp.json have to be the answer. They ride in on
// `serverSettings.roots` (lifted by `mcpConfigToServerEntries`), which is
// what web passes too — a CLI that seeded `[]` would let a server fall
// back to its own defaults, the outcome #1797 is about.
const server = createTestServerHttp({
serverInfo: createTestServerInfo(),
tools: [createListRootsTool()],
});
let configPath: string | undefined;
try {
await server.start();
configPath = createTestConfig({
mcpServers: {
web: {
type: "streamable-http",
url: server.url,
roots: [{ uri: "file:///configured", name: "Configured" }],
} as unknown as MCPServerConfig,
},
});

const result = await runCli([
"--config",
configPath,
"--server",
"web",
"--cli",
"--method",
"tools/call",
"--tool-name",
"list_roots",
]);

expectCliSuccess(result);
const json = expectValidJson(result);
expect(json.isError).toBeFalsy();
expect(JSON.stringify(json)).toContain("file:///configured");
} finally {
await server.stop();
if (configPath) deleteConfigFile(configPath);
}
});
});

describe("Config-file settings lifting (#1482)", () => {
it("applies a config file's custom header on a tools/call over HTTP", async () => {
const server = createTestServerHttp({
Expand Down
32 changes: 31 additions & 1 deletion clients/cli/__tests__/run-method.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { describe, it, expect, afterEach } from "vitest";
import { describe, it, expect, afterEach, vi } from "vitest";
import { getTestMcpServerCommand } from "@modelcontextprotocol/inspector-test-server";
import { InspectorClient } from "@inspector/core/mcp/index.js";
import { createTransportNode } from "@inspector/core/mcp/node/index.js";
Expand All @@ -25,6 +25,9 @@ describe("runMethod", () => {
progress: false,
sample: false,
elicit: false,
// Mirrors `cli.ts`, which always passes the option so the capability is
// negotiated and the `roots/list` handler registered (#1797).
roots: [],
},
);
await client.connect();
Expand Down Expand Up @@ -92,6 +95,33 @@ describe("runMethod", () => {
}
});

it("roots/set drops a malformed root rather than advertising it", async () => {
// `--roots-json` only checks that the payload parses to an array, so a
// root with no `uri` used to be stored and then handed to the server in
// the `roots/list` reply — an invalid Root on the wire, echoed back to the
// user as success. `setRoots` normalizes through `cleanRoots` now (#1797).
const c = await connectStdio();
// cleanRoots reports the drop; suppress it here and assert it happened, so
// the test also covers that a dropped root is not dropped silently.
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
try {
const set = await runMethod(c, {
method: "roots/set",
rootsJson: JSON.stringify([
{ name: "no uri" },
{ uri: "file:///keep" },
]),
});
expect(set.kind).toBe("result");
if (set.kind === "result") {
expect(set.result.roots).toEqual([{ uri: "file:///keep" }]);
}
expect(warn).toHaveBeenCalled();
} finally {
warn.mockRestore();
}
});

it("consumeMethodOutcome writes result json", async () => {
let stdout = "";
const original = process.stdout.write;
Expand Down
10 changes: 10 additions & 0 deletions clients/cli/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { listServerEntries, showServerEntry } from "./handlers/servers-list.js";
import { writeFormattedResult } from "./handlers/format-output.js";
import { clearStoredAuthForRelogin } from "./clear-stored-auth-for-relogin.js";
import { InspectorClient } from "@inspector/core/mcp/index.js";
import { cleanRoots } from "@inspector/core/mcp/serverList.js";
import {
createTransportNode,
loadServerEntries,
Expand Down Expand Up @@ -152,6 +153,15 @@ async function callMethod(
progress: false,
sample: false,
elicit: false,
// Advertise the roots configured for this server in mcp.json, exactly as
// web does (`App.tsx`) so both answer `roots/list` with the same content.
// Passing the option (even empty) is what negotiates `capabilities.roots`
// at `initialize` and registers the `roots/list` handler. Omitting it meant
// a server that asks for roots on its own — `server-filesystem` does, at
// `initialize` — got -32601, and `--method roots/set` could not announce
// the change at all: the SDK refuses `roots/list_changed` from a client
// that never declared it, which `setRoots` logged as a send failure (#1797).
roots: cleanRoots(serverSettings?.roots ?? []),
serverSettings,
// Per-server protocol era (SEP §7.8) from mcp.json → SDK versionNegotiation.
// Absent era defaults to legacy in the InspectorClient constructor (#1626).
Expand Down
6 changes: 6 additions & 0 deletions clients/tui/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import type {
GetPromptResult,
} from "@modelcontextprotocol/client";
import { InspectorClient } from "@inspector/core/mcp/index.js";
import { cleanRoots } from "@inspector/core/mcp/serverList.js";
import { eraToVersionNegotiation } from "@inspector/core/mcp/types.js";
import {
ManagedToolsState,
Expand Down Expand Up @@ -304,6 +305,11 @@ function App({
defaultMetadata,
}),
...(savedSettings && { serverSettings: savedSettings }),
// Advertise the roots configured for this server in mcp.json, as web
// and the CLI do. The TUI has no roots editor, but a user who set
// roots in the web UI expects them to apply to the same server here
// (#1797).
roots: cleanRoots(savedSettings?.roots ?? []),
// Per-server protocol era (SEP §7.8) from mcp.json → SDK
// versionNegotiation; absent era defaults to legacy (#1626).
...(savedSettings?.protocolEra && {
Expand Down
28 changes: 23 additions & 5 deletions clients/web/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,9 @@ import type {
} from "@inspector/core/mcp/types.js";
import {
DEFAULT_MAX_FETCH_REQUESTS,
DEFAULT_MODERN_LOG_LEVEL,
DEFAULT_TASK_TTL_MS,
eraToVersionNegotiation,
resolveModernLogLevel,
} from "@inspector/core/mcp/types.js";
import {
API_SERVER_ENV_VARS,
Expand Down Expand Up @@ -1216,7 +1216,27 @@ function App() {
setConsoleUi(EMPTY_CONSOLE_UI);
setProgressByTaskId({});
setCurrentLogLevel("info");
setModernLogLevel(null);
// Re-seed rather than blank: the client restores its own opt-in from the
// server setting at connect (`resetSessionState`), so blanking here would
// leave the control reading Off while every modern request still carries
// the level — visible on the auth-recovery path, which reconnects the same
// client instance rather than rebuilding it (#1629, #1797).
// `activeServerIdRef` is synced in a passive effect, so it still holds the
// outgoing server's id when this runs from `onDisconnect` — which is what
// lets the re-seed find its settings. Clearing that ref eagerly would take
// the no-server branch below and silently drop this to Off.
// Branch on the *server*, not its settings: an entry with no settings node
// is the common case (`mcp.json` written by hand, never opened in Server
// Settings), and there the default is right — it is what the seed and the
// client both use. Only "no server at all" means Off.
const activeServer = serversRef.current.find(
(s) => s.id === activeServerIdRef.current,
);
setModernLogLevel(
activeServer
? (resolveModernLogLevel(activeServer.settings) ?? null)
: null,
);
setPendingStepUp(null);
setPendingReauth(null);
setReAuthBanner(null);
Expand Down Expand Up @@ -2337,9 +2357,7 @@ function App() {
// setting so the Logs-tab control reflects what the client stamps by
// default (the client was seeded the same way in its constructor). "off"
// means not opted in (null). Only affects modern connections.
const seededModernLevel =
savedSettings?.modernLogLevel ?? DEFAULT_MODERN_LOG_LEVEL;
setModernLogLevel(seededModernLevel === "off" ? null : seededModernLevel);
setModernLogLevel(resolveModernLogLevel(savedSettings) ?? null);
setManagedToolsState(new ManagedToolsState(client));
setPagedToolsState(new PagedToolsState(client));
setPagedPromptsState(new PagedPromptsState(client));
Expand Down
Loading
Loading