Merge remote-tracking branch 'origin/v2/main' into v2/fix/2297-401-response-body

Signed-off-by: cliffhall <cliff@futurescale.com>

# Conflicts:
#	core/mcp/node/transport.ts
This commit is contained in:
cliffhall 2026-09-14 01:47:25 -04:00
commit 860b8d27ad
79 changed files with 4402 additions and 100 deletions

View file

@ -76,6 +76,7 @@ Each client has its own README with client-specific detail:
| [Publishing](./docs/publishing.md) | What ships in the tarball, the packaging invariants, and `pack:verify` | | [Publishing](./docs/publishing.md) | What ships in the tarball, the packaging invariants, and `pack:verify` |
| [Docker](./docs/docker.md) | Running the container image — ports, volumes, and where secrets go | | [Docker](./docs/docker.md) | Running the container image — ports, volumes, and where secrets go |
| [Migrating from v1 to v2](./docs/v1-to-v2-migration.md) | CLI flag mapping, `--config` vs. `--catalog`, the Node engine bump, env-var renames | | [Migrating from v1 to v2](./docs/v1-to-v2-migration.md) | CLI flag mapping, `--config` vs. `--catalog`, the Node engine bump, env-var renames |
| [Environment variables](./docs/environment-variables.md) | Every variable that changes runtime behavior — auth, ports, storage, the secret store, logging, proxies — plus the Node TLS variables for a self-signed server |
| [MCP server configuration](./docs/mcp-server-configuration.md) | Which server(s) the Inspector connects to, and the config file format | | [MCP server configuration](./docs/mcp-server-configuration.md) | Which server(s) the Inspector connects to, and the config file format |
| [Reviewing an MCP App](./docs/mcp-app-review.md) | The CLI-first → one-shot-web recipe for automated App-tool review | | [Reviewing an MCP App](./docs/mcp-app-review.md) | The CLI-first → one-shot-web recipe for automated App-tool review |
| [Smoke-testing an MCP server](./docs/cli-smoke-testing.md) | The connect → list → call → assert workflow for a shell or CI job: `--format json` + `jq`, the exit-code map, and keeping OAuth non-interactive | | [Smoke-testing an MCP server](./docs/cli-smoke-testing.md) | The connect → list → call → assert workflow for a shell or CI job: `--format json` + `jq`, the exit-code map, and keeping OAuth non-interactive |

View file

@ -84,7 +84,7 @@ When a server is loaded from a `--catalog`/`--config` file, its per-server setti
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. 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. **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. Every variable the CLI reads is listed in [Environment variables](../../docs/environment-variables.md).
### HTTP proxy support ### HTTP proxy support
@ -102,7 +102,7 @@ Because undici's `Response` is a different class from `globalThis.Response`, the
### MCP server (which server to connect to) ### MCP server (which server to connect to)
Options that specify the MCP server (catalog/config file, ad-hoc command/URL, env vars, headers) are shared by the Web, CLI, and TUI and are documented in [MCP server configuration](../../docs/mcp-server-configuration.md): `--catalog` (writable catalog, seeded **empty** if missing; default `~/.mcp-inspector/mcp.json` or `MCP_CATALOG_PATH`), `--config` (read-only session, errors if absent), `--server`, `-e`, `--cwd`, `--header`, `--transport`, `--server-url`, and the positional `[target...]`. `--catalog` and `--config` are mutually exclusive, and neither combines with an ad-hoc target. Options that specify the MCP server (catalog/config file, ad-hoc command/URL, env vars, headers) are shared by the Web, CLI, and TUI and are documented in [MCP server configuration](../../docs/mcp-server-configuration.md): `--catalog` (writable catalog, seeded **empty** if missing; default `~/.mcp-inspector/mcp.json` or `MCP_CATALOG_PATH`), `--config` (read-only session, errors if absent), `--server`, `-e`, `--cwd`, `--header`, `--protocol-era` (`legacy`/`auto`/`modern`; sets the era an ad-hoc run negotiates, or overrides a file's `protocolEra`), `--transport`, `--server-url`, and the positional `[target...]`. `--catalog` and `--config` are mutually exclusive, and neither combines with an ad-hoc target.
### CLI-specific (what to invoke) ### CLI-specific (what to invoke)
@ -391,7 +391,12 @@ not read has not been cleared of anything:
| Per skill, on the wire | 16 MiB actually served | The declared sizes are server-controlled; this one cannot be lied past. | | Per skill, on the wire | 16 MiB actually served | The declared sizes are server-controlled; this one cannot be lied past. |
| Per run | 256 skills / 64 MiB | SEP-2640 bounds a skill and deliberately does not bound a *catalog*. Every entry costs at least one `resources/read`, so without this a large listing — hostile or merely big — is unbounded work against the tool inspecting it. | | Per run | 256 skills / 64 MiB | SEP-2640 bounds a skill and deliberately does not bound a *catalog*. Every entry costs at least one `resources/read`, so without this a large listing — hostile or merely big — is unbounded work against the tool inspecting it. |
The run bound is this tool's, not the spec's. A skill past it is still reported, The run bound is this tool's, not the spec's, and it is configurable per server:
set `skillCatalogMaxSkills` / `skillCatalogMaxBytes` on the server's entry in
`mcp.json`, or edit them under **Skills** in the web client's Server Settings
(see [the configuration reference](../../docs/mcp-server-configuration.md)).
Both must be positive integers — there is no unlimited value, since the bound
is what makes the run terminate. A skill past it is still reported,
with its static conformance findings and an `incomplete` reason saying nothing with its static conformance findings and an `incomplete` reason saying nothing
about its files was checked; verify it on its own with `--method skills/get about its files was checked; verify it on its own with `--method skills/get
--uri <skill>` to get a verdict for it. --uri <skill>` to get a verdict for it.

View file

@ -1,8 +1,16 @@
import { describe, it, expect } from "vitest"; import { describe, it, expect } from "vitest";
import { getTestMcpServerCommand } from "@modelcontextprotocol/inspector-test-server"; import {
createEchoTool,
createTestServerHttp,
createTestServerInfo,
getTestMcpServerCommand,
} from "@modelcontextprotocol/inspector-test-server";
import { runCli } from "./helpers/cli-runner.js"; import { runCli } from "./helpers/cli-runner.js";
import { DEFAULT_CONNECT_TIMEOUT_MS, withConnectTimeout } from "../src/cli.js"; import { DEFAULT_CONNECT_TIMEOUT_MS, withConnectTimeout } from "../src/cli.js";
import type { InspectorServerSettings } from "@inspector/core/mcp/types.js"; import {
MODERN_PROTOCOL_VERSION,
type InspectorServerSettings,
} from "@inspector/core/mcp/types.js";
import { import {
expectCliFailure, expectCliFailure,
expectCliSuccess, expectCliSuccess,
@ -227,6 +235,70 @@ describe("CLI --stored-auth-only wiring", () => {
}); });
}); });
describe("--protocol-era", () => {
// The discriminating case: `legacy` and `auto` both connect to a legacy
// fixture whether or not the flag is wired, since `auto` falls back. A
// modern-only server rejects the default legacy handshake, so only a flag
// that actually reaches negotiation turns the failure into a modern session.
it("negotiates the modern era against a modern-only server", async () => {
const server = createTestServerHttp({
serverInfo: createTestServerInfo(),
tools: [createEchoTool()],
modern: { legacy: "reject" },
});
try {
await server.start();
const adHoc = [server.url, "--transport", "http"];
const withoutFlag = await runCli([...adHoc, "--method", "initialize"]);
expectCliFailure(withoutFlag);
const withFlag = await runCli([
...adHoc,
"--protocol-era",
"modern",
"--method",
"initialize",
]);
expectCliSuccess(withFlag);
const json = JSON.parse(withFlag.stdout) as { protocolVersion: string };
expect(json.protocolVersion).toBe(MODERN_PROTOCOL_VERSION);
} finally {
await server.stop();
}
});
it.each(["legacy", "auto"])(
"accepts %s on an ad-hoc target and connects",
async (era) => {
const { command, args } = getTestMcpServerCommand();
const result = await runCli([
command,
...args,
"--protocol-era",
era,
"--method",
"tools/list",
]);
expectCliSuccess(result);
},
);
it("rejects an unknown era before connecting", async () => {
const { command, args } = getTestMcpServerCommand();
const result = await runCli([
command,
...args,
"--protocol-era",
"future",
"--method",
"tools/list",
]);
expectCliFailure(result);
expect(result.stderr).toContain("Invalid protocol era: future");
});
});
describe("MCP_CATALOG_PATH with an ad-hoc target", () => { describe("MCP_CATALOG_PATH with an ad-hoc target", () => {
it("does not conflict with an ad-hoc target (env catalog is ignored)", async () => { it("does not conflict with an ad-hoc target (env catalog is ignored)", async () => {
const { command, args } = getTestMcpServerCommand(); const { command, args } = getTestMcpServerCommand();

View file

@ -5,6 +5,7 @@ import type {
InspectorServerSettings, InspectorServerSettings,
MCPServerConfig, MCPServerConfig,
InspectorClientEnvironment, InspectorClientEnvironment,
ServerProtocolEra,
} from "@inspector/core/mcp/types.js"; } from "@inspector/core/mcp/types.js";
import { eraToVersionNegotiation } from "@inspector/core/mcp/types.js"; import { eraToVersionNegotiation } from "@inspector/core/mcp/types.js";
import { import {
@ -23,6 +24,7 @@ import {
selectServerEntry, selectServerEntry,
parseKeyValuePair as parseEnvPair, parseKeyValuePair as parseEnvPair,
parseHeaderPair, parseHeaderPair,
parseProtocolEra,
} from "@inspector/core/mcp/node/index.js"; } from "@inspector/core/mcp/node/index.js";
import type { JsonValue } from "@inspector/core/mcp/index.js"; import type { JsonValue } from "@inspector/core/mcp/index.js";
import type { StrictJsonValue } from "@inspector/core/json/jsonUtils.js"; import type { StrictJsonValue } from "@inspector/core/json/jsonUtils.js";
@ -773,6 +775,11 @@ async function parseArgs(argv?: string[]): Promise<ParseResult> {
return n; return n;
}, },
) )
.option(
"--protocol-era <era>",
"Protocol era to negotiate: legacy, auto, or modern. Overrides the file-level protocolEra for --catalog/--config runs; ad-hoc --server-url / target runs otherwise default to legacy.",
parseProtocolEra,
)
.option( .option(
"--format <format>", "--format <format>",
"Output format: text (default; pretty-printed) or json (one JSON object on stdout, no banners).", "Output format: text (default; pretty-printed) or json (one JSON object on stdout, no banners).",
@ -870,6 +877,7 @@ async function parseArgs(argv?: string[]): Promise<ParseResult> {
verify?: boolean; verify?: boolean;
cursor?: string; cursor?: string;
connectTimeout?: number; connectTimeout?: number;
protocolEra?: ServerProtocolEra;
format?: OutputFormat; format?: OutputFormat;
toolArgsJson?: string; toolArgsJson?: string;
clientConfig?: string; clientConfig?: string;
@ -1017,6 +1025,9 @@ async function parseArgs(argv?: string[]): Promise<ParseResult> {
// `--header` is merged into the resolved server's settings (overriding any // `--header` is merged into the resolved server's settings (overriding any
// file-level headers); file timeouts/OAuth are preserved. See #1482. // file-level headers); file timeouts/OAuth are preserved. See #1482.
headers: options.header as Record<string, string> | undefined, headers: options.header as Record<string, string> | undefined,
// `--protocol-era` feeds `settings.protocolEra` the same way, so an ad-hoc
// launch can pick a non-legacy era without an mcp.json entry (#2208).
protocolEra: options.protocolEra,
}; };
// Catalog list / show — no MCP connection. Run before stored-auth refresh so // Catalog list / show — no MCP connection. Run before stored-auth refresh so

View file

@ -8,9 +8,9 @@ The launcher is the package that provides the global `mcp-inspector` binary (e.g
- Forward all following arguments unchanged (including tokens that look like mode flags). - Forward all following arguments unchanged (including tokens that look like mode flags).
- Dynamically import that app’s runner from `clients/{web,cli,tui}/build/index.js` (relative to the launcher build output) and call it **in-process** (no `spawn()`). - Dynamically import that app’s runner from `clients/{web,cli,tui}/build/index.js` (relative to the launcher build output) and call it **in-process** (no `spawn()`).
All configuration parsing, config-file loading, and server setup are handled by the app runners and by **core**; the launcher does not interpret config or env vars. All configuration parsing, config-file loading, and server setup are handled by the app runners and by **core**; the launcher does not interpret server or client configuration, from files or from env vars. Its one exception is the debug switch described below.
**Error reporting.** A `--cli` failure is routed through the CLI's own error sink, so `mcp-inspector --cli` preserves the CLI exit-code map (`1` usage, `2` no-app, `3` auth-required, `4` unreachable, `5` tool-error) and its machine-readable `{"error":{…}}` stderr envelope — the same as invoking the CLI bin directly. `--web` / `--tui` failures print a human-readable `Error: <message>` and exit `1` (append `MCP_DEBUG=1` for the stack). **Error reporting.** A `--cli` failure is routed through the CLI's own error sink, so `mcp-inspector --cli` preserves the CLI exit-code map (`1` usage, `2` no-app, `3` auth-required, `4` unreachable, `5` tool-error) and its machine-readable `{"error":{…}}` stderr envelope — the same as invoking the CLI bin directly. `--web` / `--tui` failures print a human-readable `Error: <message>` and exit `1` (append `MCP_DEBUG=1` for the stack). `MCP_DEBUG` (or `DEBUG`) is the only variable the launcher reads itself; the rest are listed in [Environment variables](../../docs/environment-variables.md).
## Web server-list flags (`--web`) ## Web server-list flags (`--web`)
@ -25,8 +25,11 @@ editable (see [specification/v2_catalog_launch_config.md](../../specification/v2
| `mcp-inspector --web --server-url <url> --transport http --header "Name: Value"` (or a positional command) | One ad-hoc server held in memory, connectable with the given `--header`s | No | | `mcp-inspector --web --server-url <url> --transport http --header "Name: Value"` (or a positional command) | One ad-hoc server held in memory, connectable with the given `--header`s | No |
Rules: `--catalog` and `--config` are mutually exclusive; neither combines with Rules: `--catalog` and `--config` are mutually exclusive; neither combines with
an ad-hoc target or `--header`; `--header` requires an ad-hoc HTTP/SSE server an ad-hoc target, `--header` or `--protocol-era`; `--header` requires an ad-hoc
and is applied to that connection (it is no longer a warn-only no-op). HTTP/SSE server and is applied to that connection (it is no longer a warn-only
no-op). `--protocol-era <legacy|auto|modern>` requires an ad-hoc server of any
transport and sets the era that connection negotiates, so a non-legacy era
needs no `mcp.json` entry.
**Seed contents are web-specific.** When the web backend creates a missing **Seed contents are web-specific.** When the web backend creates a missing
writable catalog it seeds `DEFAULT_SEED_CONFIG` (`core/mcp/serverList.ts`) — a writable catalog it seeds `DEFAULT_SEED_CONFIG` (`core/mcp/serverList.ts`) — a

View file

@ -29,7 +29,7 @@ npx @modelcontextprotocol/inspector --tui --config mcp.json # read-only sessi
### MCP server (which server(s) to connect to) ### MCP server (which server(s) to connect to)
Options that specify the MCP server(s) (catalog/config file, ad-hoc command/URL, env vars, headers) are shared by the Web, CLI, and TUI and are documented in [MCP server configuration](../../docs/mcp-server-configuration.md): `--catalog` (writable catalog, seeded **empty** if missing; default `~/.mcp-inspector/mcp.json` or `MCP_CATALOG_PATH`), `--config` (read-only session, errors if absent), `-e`, `--cwd`, `--header`, `--transport`, `--server-url`, and the positional `[target...]`. `--catalog` and `--config` are mutually exclusive, and neither combines with an ad-hoc target. Options that specify the MCP server(s) (catalog/config file, ad-hoc command/URL, env vars, headers) are shared by the Web, CLI, and TUI and are documented in [MCP server configuration](../../docs/mcp-server-configuration.md): `--catalog` (writable catalog, seeded **empty** if missing; default `~/.mcp-inspector/mcp.json` or `MCP_CATALOG_PATH`), `--config` (read-only session, errors if absent), `-e`, `--cwd`, `--header`, `--protocol-era` (`legacy`/`auto`/`modern`; sets the era an ad-hoc server negotiates, or overrides a file's `protocolEra`), `--transport`, `--server-url`, and the positional `[target...]`. `--catalog` and `--config` are mutually exclusive, and neither combines with an ad-hoc target.
### TUI-specific (OAuth for HTTP servers) ### TUI-specific (OAuth for HTTP servers)
@ -52,6 +52,8 @@ OAuth redirect URIs must match **exactly** what you register on the authorizatio
#### Flags #### Flags
Variables with no flag of their own — the secret store, `MCP_INSPECTOR_LOG_DIR`, `LOG_LEVEL`, proxies — are listed in [Environment variables](../../docs/environment-variables.md).
| Option | Env | Description | | Option | Env | Description |
| ----------------------------- | ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | ----------------------------- | ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `--client-config <path>` | `MCP_CLIENT_CONFIG_PATH` | Install-level client config (default: `~/.mcp-inspector/storage/client.json`). | | `--client-config <path>` | `MCP_CLIENT_CONFIG_PATH` | Install-level client config (default: `~/.mcp-inspector/storage/client.json`). |

View file

@ -5,7 +5,9 @@ import { render } from "ink";
import { import {
parseKeyValuePair, parseKeyValuePair,
parseHeaderPair, parseHeaderPair,
parseProtocolEra,
} from "@inspector/core/mcp/node/index.js"; } from "@inspector/core/mcp/node/index.js";
import type { ServerProtocolEra } from "@inspector/core/mcp/types.js";
import { loadRunnerClientConfig } from "@inspector/core/client/runner.js"; import { loadRunnerClientConfig } from "@inspector/core/client/runner.js";
import { import {
parseRunnerOAuthCallbackUrl, parseRunnerOAuthCallbackUrl,
@ -41,6 +43,11 @@ export async function runTui(args?: string[]): Promise<void> {
parseHeaderPair, parseHeaderPair,
{}, {},
) )
.option(
"--protocol-era <era>",
"Protocol era to negotiate: legacy, auto, or modern (overrides the file's protocolEra; default legacy)",
parseProtocolEra,
)
.option( .option(
"--client-id <id>", "--client-id <id>",
"OAuth client ID (static client) for HTTP servers", "OAuth client ID (static client) for HTTP servers",
@ -78,6 +85,7 @@ export async function runTui(args?: string[]): Promise<void> {
e?: Record<string, string>; e?: Record<string, string>;
cwd?: string; cwd?: string;
header?: Record<string, string>; header?: Record<string, string>;
protocolEra?: ServerProtocolEra;
clientId?: string; clientId?: string;
clientSecret?: string; clientSecret?: string;
clientMetadataUrl?: string; clientMetadataUrl?: string;
@ -95,6 +103,7 @@ export async function runTui(args?: string[]): Promise<void> {
cwd: options.cwd?.trim() || undefined, cwd: options.cwd?.trim() || undefined,
env: options.e, env: options.e,
headers: options.header, headers: options.header,
protocolEra: options.protocolEra,
transport: options.transport, transport: options.transport,
serverUrl: options.serverUrl?.trim() || undefined, serverUrl: options.serverUrl?.trim() || undefined,
}; };

View file

@ -384,7 +384,7 @@ Storybook is first-class here because the components are presentational — each
## Auth token ## Auth token
The dev/prod backend guards every `/api/*` route with `x-mcp-remote-auth: Bearer <MCP_INSPECTOR_API_TOKEN>`. The browser recovers the token, in priority order (see `App.tsx` `getAuthToken()`): the `window.__INSPECTOR_API_TOKEN__` global injected into `index.html` on every page load (`server/inject-auth-token.ts`), then a `?MCP_INSPECTOR_API_TOKEN=…` query param, then `sessionStorage`. Injection is a no-op when auth is disabled (`DANGEROUSLY_OMIT_AUTH`). See the root [AGENTS.md](../../AGENTS.md) for the full rationale. The dev/prod backend guards every `/api/*` route with `x-mcp-remote-auth: Bearer <MCP_INSPECTOR_API_TOKEN>`. The browser recovers the token, in priority order (see `App.tsx` `getAuthToken()`): the `window.__INSPECTOR_API_TOKEN__` global injected into `index.html` on every page load (`server/inject-auth-token.ts`), then a `?MCP_INSPECTOR_API_TOKEN=…` query param, then `sessionStorage`. Injection is a no-op when auth is disabled (`DANGEROUSLY_OMIT_AUTH`). See the root [AGENTS.md](../../AGENTS.md) for the full rationale, and [Environment variables](../../docs/environment-variables.md) for every variable the backend reads.
## Host binding & the origin allow-list ## Host binding & the origin allow-list

View file

@ -10,12 +10,14 @@ import { Command } from "commander";
import type { import type {
MCPConfig, MCPConfig,
MCPServerConfig, MCPServerConfig,
ServerProtocolEra,
StoredMCPServer, StoredMCPServer,
} from "../../../core/mcp/types.ts"; } from "../../../core/mcp/types.ts";
import { import {
resolveServerConfigs, resolveServerConfigs,
parseKeyValuePair, parseKeyValuePair,
parseHeaderPair, parseHeaderPair,
parseProtocolEra,
type ServerConfigOptions, type ServerConfigOptions,
} from "../../../core/mcp/node/config.ts"; } from "../../../core/mcp/node/config.ts";
import { import {
@ -89,6 +91,11 @@ export async function runWeb(argv: string[]): Promise<number> {
parseHeaderPair, parseHeaderPair,
{}, {},
) )
.option(
"--protocol-era <era>",
"protocol era for the ad-hoc server: legacy, auto, or modern (default legacy)",
parseProtocolEra,
)
.option("--dev", "run in development mode (Vite)") .option("--dev", "run in development mode (Vite)")
.parse(preArgs); .parse(preArgs);
@ -101,6 +108,7 @@ export async function runWeb(argv: string[]): Promise<number> {
serverUrl?: string; serverUrl?: string;
cwd?: string; cwd?: string;
header?: Record<string, string>; header?: Record<string, string>;
protocolEra?: ServerProtocolEra;
dev?: boolean; dev?: boolean;
}; };
@ -124,6 +132,10 @@ export async function runWeb(argv: string[]): Promise<number> {
const hasCatalog = !!catalogPath; const hasCatalog = !!catalogPath;
const hasConfig = !!opts.config; const hasConfig = !!opts.config;
const hasHeaders = !!opts.header && Object.keys(opts.header).length > 0; const hasHeaders = !!opts.header && Object.keys(opts.header).length > 0;
// Like `--header`, web applies `--protocol-era` only to an ad-hoc server: a
// catalog/config entry carries its own `protocolEra`, editable in Server
// Settings, and web never overlays launch flags onto a file's settings.
const hasProtocolEra = opts.protocolEra !== undefined;
const hasAdHocServer = const hasAdHocServer =
target.length > 0 || target.length > 0 ||
!!opts.serverUrl || !!opts.serverUrl ||
@ -137,11 +149,15 @@ export async function runWeb(argv: string[]): Promise<number> {
? "--catalog cannot be combined with an ad-hoc server URL/command." ? "--catalog cannot be combined with an ad-hoc server URL/command."
: hasCatalog && hasHeaders : hasCatalog && hasHeaders
? "--header cannot be combined with --catalog. Set per-server headers in the catalog file." ? "--header cannot be combined with --catalog. Set per-server headers in the catalog file."
: hasConfig && hasAdHocServer : hasCatalog && hasProtocolEra
? "--config cannot be combined with an ad-hoc server URL/command. --config selects a read-only session file." ? "--protocol-era cannot be combined with --catalog. Set protocolEra per server in the catalog file."
: hasConfig && hasHeaders : hasConfig && hasAdHocServer
? "--header cannot be combined with --config. Set per-server headers inside the config file instead." ? "--config cannot be combined with an ad-hoc server URL/command. --config selects a read-only session file."
: null; : hasConfig && hasHeaders
? "--header cannot be combined with --config. Set per-server headers inside the config file instead."
: hasConfig && hasProtocolEra
? "--protocol-era cannot be combined with --config. Set protocolEra per server inside the config file instead."
: null;
if (conflict) { if (conflict) {
console.error(`Error: ${conflict}`); console.error(`Error: ${conflict}`);
process.exit(1); process.exit(1);
@ -217,13 +233,18 @@ export async function runWeb(argv: string[]): Promise<number> {
initialMcpConfig = config; initialMcpConfig = config;
// In-memory session entry: the SDK config plus the flat `headers` record, // In-memory session entry: the SDK config plus the flat `headers` record
// which the backend lifts into `settings.headers` on read. Nothing is // and `protocolEra`, which the backend lifts into `settings` on read.
// written to disk — the ad-hoc server is a read-only session list. // Nothing is written to disk — the ad-hoc server is a read-only session
// list. The era is transport-agnostic, so unlike `--header` it applies to
// stdio too.
const entry: StoredMCPServer = const entry: StoredMCPServer =
hasHeaders && config.type !== "stdio" && config.type !== undefined hasHeaders && config.type !== "stdio" && config.type !== undefined
? { ...config, headers: opts.header } ? { ...config, headers: opts.header }
: { ...config }; : { ...config };
if (opts.protocolEra !== undefined) {
entry.protocolEra = opts.protocolEra;
}
initialServers = { initialServers = {
mcpServers: { [deriveSeedServerId(config)]: entry }, mcpServers: { [deriveSeedServerId(config)]: entry },
}; };
@ -233,6 +254,11 @@ export async function runWeb(argv: string[]): Promise<number> {
"Error: --header requires an ad-hoc --server-url or command target.", "Error: --header requires an ad-hoc --server-url or command target.",
); );
process.exit(1); process.exit(1);
} else if (hasProtocolEra) {
console.error(
"Error: --protocol-era requires an ad-hoc --server-url or command target.",
);
process.exit(1);
} }
let webConfig: WebServerConfig; let webConfig: WebServerConfig;

View file

@ -714,6 +714,7 @@ const DEFAULT_USE_INSPECTOR_CLIENT: ReturnType<typeof useInspectorClient> = {
instructions: undefined, instructions: undefined,
excludedTools: [], excludedTools: [],
malformedListItems: [], malformedListItems: [],
connectionDiagnostics: { capturedAt: 0, outstandingRequests: [] },
appRendererClient: null, appRendererClient: null,
connect: vi.fn().mockResolvedValue(undefined), connect: vi.fn().mockResolvedValue(undefined),
disconnect: vi.fn().mockResolvedValue(undefined), disconnect: vi.fn().mockResolvedValue(undefined),

View file

@ -401,6 +401,7 @@ function App() {
discoverResult, discoverResult,
excludedTools, excludedTools,
malformedListItems, malformedListItems,
connectionDiagnostics,
lastError, lastError,
} = useInspectorClient(inspectorClient); } = useInspectorClient(inspectorClient);
// What every settings write that landed on disk actually wrote, so a later // What every settings write that landed on disk actually wrote, so a later
@ -2028,6 +2029,7 @@ function App() {
transport={connectionInfoTransport} transport={connectionInfoTransport}
protocolEra={protocolEra} protocolEra={protocolEra}
discoverResult={discoverResult} discoverResult={discoverResult}
diagnostics={connectionDiagnostics}
oauth={connectionInfoOAuth} oauth={connectionInfoOAuth}
onClearOAuth={ onClearOAuth={
connectionInfoCanClearOAuth ? handleClearConnectionOAuth : undefined connectionInfoCanClearOAuth ? handleClearConnectionOAuth : undefined

View file

@ -46,6 +46,51 @@ export const FullCapabilities: Story = {
}, },
}; };
// The snapshot clock, read once when the stories load. The rows start from it
// and then tick against the wall clock, so the fixture has to be "now" — a
// fixed historical date would read as months ago a second after mounting.
const STORY_NOW = Date.now();
/**
* The Connection Activity section, in the #2187 shape: the notification
* stream has been open the whole session and delivered nothing, `tools/list`
* has been outstanding for a minute, and nothing has been answered since
* `initialize`. The same facts a request timeout's message reports, shown
* while the request is still in flight (#2318).
*/
export const WithConnectionActivity: Story = {
args: {
initializeResult: fullResult,
clientCapabilities: fullClientCaps,
transport: "streamable-http",
diagnostics: {
capturedAt: STORY_NOW,
outstandingRequests: [
{ id: 2, method: "tools/list", sentAt: STORY_NOW - 60_000 },
{ id: 3, method: "ping", sentAt: STORY_NOW - 12_000 },
],
lastResponse: { method: "initialize", receivedAt: STORY_NOW - 61_000 },
notificationStream: {
url: "http://127.0.0.1:9779/mcp",
openedAt: STORY_NOW - 252_000,
eventCount: 0,
},
},
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await expect(canvas.getByText("Connection Activity")).toBeInTheDocument();
// Ticking: the rows may have advanced a second or two since the fixture
// was stamped, so match the shape rather than the exact second.
await expect(
canvas.getByText(/^tools\/list — sent 1m0\ds ago$/),
).toBeInTheDocument();
await expect(
canvas.getByText(/^GET \/mcp — open for 4m1\ds, 0 events delivered$/),
).toBeInTheDocument();
},
};
export const ModernEra: Story = { export const ModernEra: Story = {
args: { args: {
initializeResult: { initializeResult: {

View file

@ -5,6 +5,13 @@ import type {
InitializeResult, InitializeResult,
} from "@modelcontextprotocol/client"; } from "@modelcontextprotocol/client";
import { renderWithMantine, screen } from "../../../test/renderWithMantine"; import { renderWithMantine, screen } from "../../../test/renderWithMantine";
import type { ConnectionDiagnostics } from "@inspector/core/mcp/connectionDiagnostics.js";
import {
NO_NOTIFICATION_STREAM_LABEL,
NO_OUTSTANDING_REQUESTS_LABEL,
NO_RESPONSE_YET_LABEL,
NO_STREAM_ON_STDIO_LABEL,
} from "../../../utils/connectionActivity";
import { import {
CLEAR_OAUTH_STATE_AND_DISCONNECT_LABEL, CLEAR_OAUTH_STATE_AND_DISCONNECT_LABEL,
ConnectionInfoContent, ConnectionInfoContent,
@ -934,3 +941,81 @@ describe("ConnectionInfoContent", () => {
expect(onClearOAuth).toHaveBeenCalledTimes(1); expect(onClearOAuth).toHaveBeenCalledTimes(1);
}); });
}); });
describe("ConnectionInfoContent connection activity (#2318)", () => {
const NOW = 1_000_000;
const seconds = (n: number) => n * 1000;
it("omits the section when no diagnostics are supplied", () => {
renderWithMantine(
<ConnectionInfoContent
initializeResult={fullResult}
clientCapabilities={fullClientCaps}
transport="streamable-http"
/>,
);
expect(screen.queryByText("Connection Activity")).not.toBeInTheDocument();
});
it("renders the idle state: nothing unanswered, nothing received, stream not opened", () => {
renderWithMantine(
<ConnectionInfoContent
initializeResult={fullResult}
clientCapabilities={fullClientCaps}
transport="streamable-http"
diagnostics={{ capturedAt: NOW, outstandingRequests: [] }}
/>,
);
expect(screen.getByText("Connection Activity")).toBeInTheDocument();
expect(screen.getByText(NO_OUTSTANDING_REQUESTS_LABEL)).toBeInTheDocument();
expect(screen.getByText(NO_RESPONSE_YET_LABEL)).toBeInTheDocument();
expect(screen.getByText(NO_NOTIFICATION_STREAM_LABEL)).toBeInTheDocument();
});
it("lists each unanswered request on its own line, with the last response and the open stream", () => {
const diagnostics: ConnectionDiagnostics = {
capturedAt: NOW,
outstandingRequests: [
{ id: 2, method: "tools/list", sentAt: NOW - seconds(60) },
{ id: 3, method: "ping", sentAt: NOW - seconds(12) },
],
lastResponse: { method: "initialize", receivedAt: NOW - seconds(61) },
notificationStream: {
url: "http://127.0.0.1:9779/mcp",
openedAt: NOW - seconds(252),
eventCount: 0,
},
};
renderWithMantine(
<ConnectionInfoContent
initializeResult={fullResult}
clientCapabilities={fullClientCaps}
transport="streamable-http"
diagnostics={diagnostics}
/>,
);
const outstanding = screen.getByTestId("connection-activity-outstanding");
expect(outstanding.children).toHaveLength(2);
expect(outstanding.children[0]?.textContent).toBe(
"tools/list — sent 1m00s ago",
);
expect(outstanding.children[1]?.textContent).toBe("ping — sent 12s ago");
expect(screen.getByText("initialize — 1m01s ago")).toBeInTheDocument();
expect(
screen.getByText("GET /mcp — open for 4m12s, 0 events delivered"),
).toBeInTheDocument();
});
it("reports the stream row as not applicable on stdio", () => {
renderWithMantine(
<ConnectionInfoContent
initializeResult={fullResult}
clientCapabilities={fullClientCaps}
transport="stdio"
diagnostics={{ capturedAt: NOW, outstandingRequests: [] }}
/>,
);
// Both the Session row and the Notification stream row read N/A (stdio).
expect(screen.getAllByText(NO_STREAM_ON_STDIO_LABEL)).toHaveLength(2);
});
});

View file

@ -18,6 +18,14 @@ import type {
ServerCapabilities, ServerCapabilities,
} from "@modelcontextprotocol/client"; } from "@modelcontextprotocol/client";
import type { ServerType } from "@inspector/core/mcp/types.js"; import type { ServerType } from "@inspector/core/mcp/types.js";
import type { ConnectionDiagnostics } from "@inspector/core/mcp/connectionDiagnostics.js";
import {
formatLastResponse,
formatNotificationStream,
formatOutstandingRequests,
NO_OUTSTANDING_REQUESTS_LABEL,
} from "../../../utils/connectionActivity";
import { useTickingClock } from "../../../hooks/useTickingClock";
import { TASKS_EXTENSION_KEY } from "@inspector/core/mcp/modernTaskSchemas.js"; import { TASKS_EXTENSION_KEY } from "@inspector/core/mcp/modernTaskSchemas.js";
import { getSkillsExtension } from "@inspector/core/mcp/skills.js"; import { getSkillsExtension } from "@inspector/core/mcp/skills.js";
import type { OAuthClientRegistrationKind } from "@inspector/core/auth/types.js"; import type { OAuthClientRegistrationKind } from "@inspector/core/auth/types.js";
@ -75,6 +83,16 @@ export interface ConnectionInfoContentProps {
* capabilities, and extensions learned up front. Undefined on legacy. (#1626) * capabilities, and extensions learned up front. Undefined on legacy. (#1626)
*/ */
discoverResult?: DiscoverResult; discoverResult?: DiscoverResult;
/**
* What the client is still waiting on, when it last heard back, and the
* state of the notification stream (#2318). Renders the Connection Activity
* section when present — the same facts a request timeout reports, shown
* here while the request is still in flight. Durations first read against
* the snapshot's own `capturedAt` and then tick once a second while the
* panel is open (`useTickingClock`), so an in-flight request's age keeps
* moving rather than freezing at the moment it went out.
*/
diagnostics?: ConnectionDiagnostics;
oauth?: OAuthDetails; oauth?: OAuthDetails;
onClearOAuth?: () => void; onClearOAuth?: () => void;
} }
@ -291,6 +309,7 @@ export function ConnectionInfoContent({
transport, transport,
protocolEra, protocolEra,
discoverResult, discoverResult,
diagnostics,
oauth, oauth,
onClearOAuth, onClearOAuth,
}: ConnectionInfoContentProps) { }: ConnectionInfoContentProps) {
@ -317,6 +336,15 @@ export function ConnectionInfoContent({
? serverInfo.version?.trim() || "—" ? serverInfo.version?.trim() || "—"
: SERVER_INFO_NOT_REPORTED_LABEL; : SERVER_INFO_NOT_REPORTED_LABEL;
// The activity rows' clock: the snapshot's own on first paint (pure), then
// the wall clock once a second so "sent 5s ago" keeps counting. No timer
// at all when there is no activity section to tick.
const now = useTickingClock(
diagnostics?.capturedAt ?? 0,
1000,
diagnostics !== undefined,
);
const serverCaps = getServerCapabilityEntries(capabilities, protocolEra); const serverCaps = getServerCapabilityEntries(capabilities, protocolEra);
const clientCaps = getCapabilityEntries( const clientCaps = getCapabilityEntries(
clientCapabilities, clientCapabilities,
@ -348,6 +376,42 @@ export function ConnectionInfoContent({
</SimpleGrid> </SimpleGrid>
</Stack> </Stack>
{diagnostics && (
<Stack gap="xs">
<SectionHeading>Connection Activity</SectionHeading>
<SimpleGrid cols={2}>
<FieldLabel>Unanswered requests</FieldLabel>
{/* A `Stack` so several in-flight requests read as a list, the
way the extension sections do; one line per request. */}
<Stack gap={2} data-testid="connection-activity-outstanding">
{diagnostics.outstandingRequests.length === 0 ? (
<ValueText>{NO_OUTSTANDING_REQUESTS_LABEL}</ValueText>
) : (
formatOutstandingRequests(diagnostics, now).map(
(line, index) => (
<ValueText key={diagnostics.outstandingRequests[index]!.id}>
{line}
</ValueText>
),
)
)}
</Stack>
<FieldLabel>Last response</FieldLabel>
<ValueText>{formatLastResponse(diagnostics, now)}</ValueText>
<FieldLabel>Notification stream</FieldLabel>
<ValueText>
{formatNotificationStream(
diagnostics.notificationStream,
transport,
now,
)}
</ValueText>
</SimpleGrid>
</Stack>
)}
{discoverResult && ( {discoverResult && (
<Stack gap="xs"> <Stack gap="xs">
<SectionHeading>Discovery</SectionHeading> <SectionHeading>Discovery</SectionHeading>

View file

@ -56,6 +56,27 @@ describe("ConnectionInfoModal", () => {
expect(screen.getByText("Be excellent to each other.")).toBeInTheDocument(); expect(screen.getByText("Be excellent to each other.")).toBeInTheDocument();
}); });
it("forwards connection diagnostics to ConnectionInfoContent (#2318)", () => {
renderWithMantine(
<ConnectionInfoModal
opened
onClose={vi.fn()}
initializeResult={initializeResult}
serverInfoReported
clientCapabilities={clientCapabilities}
transport="streamable-http"
diagnostics={{
capturedAt: 1_000_000,
outstandingRequests: [
{ id: 2, method: "tools/list", sentAt: 1_000_000 - 60_000 },
],
}}
/>,
);
expect(screen.getByText("Connection Activity")).toBeInTheDocument();
expect(screen.getByText("tools/list — sent 1m00s ago")).toBeInTheDocument();
});
it("forwards oauth details to ConnectionInfoContent", () => { it("forwards oauth details to ConnectionInfoContent", () => {
renderWithMantine( renderWithMantine(
<ConnectionInfoModal <ConnectionInfoModal

View file

@ -6,6 +6,7 @@ import type {
ProtocolEra, ProtocolEra,
} from "@modelcontextprotocol/client"; } from "@modelcontextprotocol/client";
import type { ServerType } from "@inspector/core/mcp/types.js"; import type { ServerType } from "@inspector/core/mcp/types.js";
import type { ConnectionDiagnostics } from "@inspector/core/mcp/connectionDiagnostics.js";
import { import {
ConnectionInfoContent, ConnectionInfoContent,
type OAuthDetails, type OAuthDetails,
@ -38,6 +39,8 @@ export interface ConnectionInfoModalProps {
transport: ServerType; transport: ServerType;
protocolEra?: ProtocolEra; protocolEra?: ProtocolEra;
discoverResult?: DiscoverResult; discoverResult?: DiscoverResult;
/** Live connection activity for the Connection Activity section (#2318). */
diagnostics?: ConnectionDiagnostics;
oauth?: OAuthDetails; oauth?: OAuthDetails;
onClearOAuth?: () => void; onClearOAuth?: () => void;
} }
@ -51,6 +54,7 @@ export function ConnectionInfoModal({
transport, transport,
protocolEra, protocolEra,
discoverResult, discoverResult,
diagnostics,
oauth, oauth,
onClearOAuth, onClearOAuth,
}: ConnectionInfoModalProps) { }: ConnectionInfoModalProps) {
@ -79,6 +83,7 @@ export function ConnectionInfoModal({
transport={transport} transport={transport}
protocolEra={protocolEra} protocolEra={protocolEra}
discoverResult={discoverResult} discoverResult={discoverResult}
diagnostics={diagnostics}
oauth={oauth} oauth={oauth}
onClearOAuth={onClearOAuth} onClearOAuth={onClearOAuth}
/> />

View file

@ -86,6 +86,31 @@ const streamingEntry: FetchRequestEntry = {
category: "transport", category: "transport",
}; };
/**
* The standalone `GET /mcp` notification stream, still open, as the tracker
* watches it: the body is never captured, so the placeholder reports what the
* stream has delivered instead (#2318).
*/
const notificationStreamEntry: FetchRequestEntry = {
id: "n-notify",
timestamp: new Date("2026-03-17T10:30:01Z"),
method: "GET",
url: "http://localhost:3000/mcp",
requestHeaders: {
accept: "text/event-stream",
"mcp-session-id": "0a0b0a5-fd27-4c95-a805-c0fba67e00fb",
},
responseStatus: 200,
responseStatusText: "OK",
responseHeaders: {
"cache-control": "no-cache",
"content-type": "text/event-stream",
},
duration: 12,
stream: { eventCount: 0 },
category: "transport",
};
const transportError: FetchRequestEntry = { const transportError: FetchRequestEntry = {
id: "n-4", id: "n-4",
timestamp: new Date("2026-03-17T10:30:15Z"), timestamp: new Date("2026-03-17T10:30:15Z"),
@ -166,6 +191,18 @@ export const StreamingResponse: Story = {
args: { entry: streamingEntry, isListExpanded: true }, args: { entry: streamingEntry, isListExpanded: true },
}; };
export const NotificationStreamOpen: Story = {
args: { entry: notificationStreamEntry, isListExpanded: true },
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await expect(
canvas.getByText(
"Long-lived stream — 0 events delivered, still open; body not captured",
),
).toBeInTheDocument();
},
};
export const FetchError: Story = { export const FetchError: Story = {
args: { entry: transportError, isListExpanded: true }, args: { entry: transportError, isListExpanded: true },
}; };

View file

@ -150,6 +150,24 @@ describe("NetworkEntry", () => {
).toBeInTheDocument(); ).toBeInTheDocument();
}); });
it("shows the watched stream's event count and open state in place of the body (#2318)", async () => {
const user = userEvent.setup();
const open: FetchRequestEntry = {
...baseEntry,
method: "GET",
responseHeaders: { "content-type": "text/event-stream" },
responseBody: undefined,
stream: { eventCount: 3 },
};
renderWithMantine(<NetworkEntry entry={open} isListExpanded={false} />);
await user.click(screen.getByRole("button", { name: "Expand" }));
expect(
screen.getByText(
"Long-lived stream — 3 events delivered, still open; body not captured",
),
).toBeInTheDocument();
});
it("shows an SSE badge on long-lived GET event-stream entries", () => { it("shows an SSE badge on long-lived GET event-stream entries", () => {
const sse: FetchRequestEntry = { const sse: FetchRequestEntry = {
...baseEntry, ...baseEntry,

View file

@ -15,7 +15,11 @@ import {
} from "@mantine/core"; } from "@mantine/core";
import { RiErrorWarningLine } from "react-icons/ri"; import { RiErrorWarningLine } from "react-icons/ri";
import type { FetchRequestEntry } from "@inspector/core/mcp/types.js"; import type { FetchRequestEntry } from "@inspector/core/mcp/types.js";
import { isLongLivedStreamResponse } from "@inspector/core/mcp/fetchTracking.js"; import {
isLongLivedStreamEntry,
longLivedStreamLabel,
uncapturedBodyNote,
} from "../../../utils/uncapturedBodyNote";
import { ContentViewer } from "../../elements/ContentViewer/ContentViewer"; import { ContentViewer } from "../../elements/ContentViewer/ContentViewer";
import { getMimeKind } from "../../elements/ContentViewer/contentViewerUtils"; import { getMimeKind } from "../../elements/ContentViewer/contentViewerUtils";
import { CopyButton } from "../../elements/CopyButton/CopyButton"; import { CopyButton } from "../../elements/CopyButton/CopyButton";
@ -158,13 +162,6 @@ function statusLabel(entry: FetchRequestEntry): string {
: `${entry.responseStatus}`; : `${entry.responseStatus}`;
} }
function isLongLivedStream(entry: FetchRequestEntry): boolean {
return isLongLivedStreamResponse(
entry.method,
entry.responseHeaders?.["content-type"],
);
}
// Header-table cell text. A modern MCP-mirrored header name gets a violet accent // Header-table cell text. A modern MCP-mirrored header name gets a violet accent
// so the spec headers (Mcp-Method / Mcp-Name / Mcp-Param-* / MCP-Protocol-Version) // so the spec headers (Mcp-Method / Mcp-Name / Mcp-Param-* / MCP-Protocol-Version)
// stand out from ordinary ones; a value that disagrees with the request body is // stand out from ordinary ones; a value that disagrees with the request body is
@ -217,6 +214,17 @@ const SentinelTooltip = Tooltip.withProps({
w: 280, w: 280,
}); });
// The long-lived-stream marker on a GET entry, on the `status` variant like
// the status chip beside it. Amber cannot clear WCAG AA as either a filled
// (white on orange, 3.04:1) or a light (orange on tint, 3.03:1) badge at this
// size; `ThemeBadge` pins `status` + orange to a mid amber fill with black
// text for exactly that reason, which is what the a11y gate needs the moment a
// story renders one (#2318 added the first).
const StreamBadge = Badge.withProps({
color: "orange",
variant: "status",
});
const Base64Badge = Badge.withProps({ const Base64Badge = Badge.withProps({
size: "xs", size: "xs",
color: "gray", color: "gray",
@ -542,7 +550,9 @@ export function NetworkEntry({
{entry.duration != null && ( {entry.duration != null && (
<DurationText>{formatDuration(entry.duration)}</DurationText> <DurationText>{formatDuration(entry.duration)}</DurationText>
)} )}
{isLongLivedStream(entry) && <Badge color="orange">SSE</Badge>} {isLongLivedStreamEntry(entry) && (
<StreamBadge>{longLivedStreamLabel(entry)}</StreamBadge>
)}
<Badge color={statusColor(entry)} variant="status"> <Badge color={statusColor(entry)} variant="status">
{statusLabel(entry)} {statusLabel(entry)}
</Badge> </Badge>
@ -647,11 +657,7 @@ export function NetworkEntry({
label="Response body" label="Response body"
/> />
) : ( ) : (
<DimmedNote> <DimmedNote>{uncapturedBodyNote(entry)}</DimmedNote>
{isLongLivedStream(entry)
? "Long-lived stream — body not captured"
: "(empty)"}
</DimmedNote>
)} )}
</Stack> </Stack>
)} )}

View file

@ -130,6 +130,12 @@ function InteractiveRender(args: ServerSettingsFormProps) {
settings: { ...args.settings, maxFetchRequests: value }, settings: { ...args.settings, maxFetchRequests: value },
}); });
}} }}
onSkillCatalogLimitChange={(field, value) => {
args.onSkillCatalogLimitChange(field, value);
updateArgs({
settings: { ...args.settings, [field]: value },
});
}}
onProtocolEraChange={(value) => { onProtocolEraChange={(value) => {
args.onProtocolEraChange(value); args.onProtocolEraChange(value);
updateArgs({ updateArgs({
@ -178,6 +184,7 @@ const meta: Meta<typeof ServerSettingsForm> = {
onPaginatedListsChange: fn(), onPaginatedListsChange: fn(),
onAdvertisedExtensionChange: fn(), onAdvertisedExtensionChange: fn(),
onMaxFetchRequestsChange: fn(), onMaxFetchRequestsChange: fn(),
onSkillCatalogLimitChange: fn(),
onProtocolEraChange: fn(), onProtocolEraChange: fn(),
onModernLogLevelChange: fn(), onModernLogLevelChange: fn(),
onOAuthChange: fn(), onOAuthChange: fn(),

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