Era-aware connection model & capability gating (#1626) (#1697)
Some checks failed
Run install, format, lint, build, and test on every push / build (push) Has been cancelled
Run install, format, lint, build, and test on every push / publish (push) Has been cancelled
Run install, format, lint, build, and test on every push / publish-github-container-registry (push) Has been cancelled
Some checks failed
Run install, format, lint, build, and test on every push / build (push) Has been cancelled
Run install, format, lint, build, and test on every push / publish (push) Has been cancelled
Run install, format, lint, build, and test on every push / publish-github-container-registry (push) Has been cancelled
* Era-aware connection model & capability gating (#1626) Make protocol era (SEP §7.8) a first-class, per-server connection dimension, orthogonal to the transport type, and drive the SDK Client's versionNegotiation from it. Config - Add `ServerProtocolEra` ("legacy" | "auto" | "modern"), `DEFAULT_PROTOCOL_ERA` (legacy, per the SDK's don't-auto-probe guidance), `MODERN_PROTOCOL_VERSION`, and `eraToVersionNegotiation()` in core/mcp/types.ts. - Persist `protocolEra` on `StoredMCPServer` / `InspectorServerSettings`, round-tripping through serverList.ts (omitting the default on disk) and the `/api/servers` read + write validators. InspectorClient / state - Thread `versionNegotiation` through `InspectorClientOptions` (default legacy), replacing the hardcoded pin. After connect, read `getProtocolEra()` / `getDiscoverResult()` / `getNegotiatedProtocolVersion()`, expose them via getters, dispatch `protocolEraChange` / `discoverResultChange`, and clear on disconnect. `useInspectorClient` surfaces `protocolEra` / `discoverResult`. Web UI - ServerSettingsForm: a Protocol Era selector bound to the per-server setting. - ConnectionInfoContent: era-aware — Era badge, Session ("Sessionless" on modern), and a Discovery section (supported versions + extensions) from the `server/discover` result. CLI/TUI also map the mcp.json `protocolEra` into versionNegotiation so the field applies uniformly. Version negotiation stays fully client-side; modern connect works over the existing remote tunnel and disconnect is sessionless (the SDK skips DELETE when there is no session id). Mcp-* header fidelity is left to the History/Network card. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5 * Address @claude review: era docstrings, era validation, stdio session label, AGENTS.md rule - Fix stale docstrings/comments in inspectorClient.ts, useInspectorClient.ts, inspectorClientEventTarget.ts, and ConnectionInfoContent.tsx: getProtocolEra() returns "legacy" (not undefined) on a plain legacy connect; it is undefined only when not connected. - Validate protocolEra at the storedFieldsToInspectorSettings read boundary (new shared isProtocolEra guard) so a hand-edited mcp.json read directly by the CLI/TUI drops an unknown era instead of passing it to eraToVersionNegotiation. Keeps the exhaustive switch intact. - ConnectionInfoContent: the Session row is HTTP-only — show "N/A (stdio)" for stdio instead of the misleading "Session-based". - AGENTS.md: add a rule against unjustified `as unknown as` double casts, and drop the one such cast introduced in the serverList test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5 * Address re-review: dedupe isProtocolEra guard in remote server Replace the local isProtocolEra in remote/node/server.ts with the shared guard now exported from serverList.ts (already imported here), so the era literal set lives in one place and can't drift. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5 --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
7fecebed5e
commit
e026051b47
3 changed files with 808 additions and 16 deletions
|
|
@ -137,6 +137,12 @@ function InteractiveRender(args: ServerSettingsFormProps) {
|
|||
settings: { ...args.settings, maxFetchRequests: value },
|
||||
});
|
||||
}}
|
||||
onProtocolEraChange={(value) => {
|
||||
args.onProtocolEraChange(value);
|
||||
updateArgs({
|
||||
settings: { ...args.settings, protocolEra: value },
|
||||
});
|
||||
}}
|
||||
onOAuthChange={(oauth) => {
|
||||
args.onOAuthChange(oauth);
|
||||
updateArgs({
|
||||
|
|
@ -173,6 +179,7 @@ const meta: Meta<typeof ServerSettingsForm> = {
|
|||
onTimeoutChange: fn(),
|
||||
onAutoRefreshChange: fn(),
|
||||
onMaxFetchRequestsChange: fn(),
|
||||
onProtocolEraChange: fn(),
|
||||
onOAuthChange: fn(),
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -66,6 +66,7 @@ const baseHandlers = {
|
|||
onTimeoutChange: vi.fn(),
|
||||
onAutoRefreshChange: vi.fn(),
|
||||
onMaxFetchRequestsChange: vi.fn(),
|
||||
onProtocolEraChange: vi.fn(),
|
||||
onOAuthChange: vi.fn(),
|
||||
onAddRoot: vi.fn(),
|
||||
onRemoveRoot: vi.fn(),
|
||||
|
|
@ -88,6 +89,46 @@ describe("ServerSettingsForm", () => {
|
|||
expect(screen.getByText("Roots")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("defaults the Protocol Era select to Legacy when unset", () => {
|
||||
renderWithMantine(
|
||||
<ServerSettingsForm
|
||||
{...baseHandlers}
|
||||
settings={emptySettings}
|
||||
expandedSections={["options"]}
|
||||
/>,
|
||||
);
|
||||
expect(
|
||||
screen.getByDisplayValue("Legacy (2025-11-25 handshake)"),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("invokes onProtocolEraChange with the selected era", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithMantine(
|
||||
<ServerSettingsForm
|
||||
{...baseHandlers}
|
||||
settings={emptySettings}
|
||||
expandedSections={["options"]}
|
||||
/>,
|
||||
);
|
||||
await user.click(screen.getByDisplayValue("Legacy (2025-11-25 handshake)"));
|
||||
await user.click(screen.getByText("Modern (2026-07-28, sessionless)"));
|
||||
expect(baseHandlers.onProtocolEraChange).toHaveBeenCalledWith("modern");
|
||||
});
|
||||
|
||||
it("reflects the configured protocolEra in the select value", () => {
|
||||
renderWithMantine(
|
||||
<ServerSettingsForm
|
||||
{...baseHandlers}
|
||||
settings={{ ...emptySettings, protocolEra: "auto" }}
|
||||
expandedSections={["options"]}
|
||||
/>,
|
||||
);
|
||||
expect(
|
||||
screen.getByDisplayValue("Auto (probe, fall back to legacy)"),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows empty hints for headers and metadata when no entries exist", () => {
|
||||
renderWithMantine(
|
||||
<ServerSettingsForm
|
||||
|
|
|
|||
|
|
@ -15,8 +15,10 @@ import { ClearButton } from "../../elements/ClearButton/ClearButton";
|
|||
import type {
|
||||
InspectorServerSettings,
|
||||
OAuthSettings,
|
||||
ServerProtocolEra,
|
||||
ServerType,
|
||||
} from "@inspector/core/mcp/types.js";
|
||||
import { DEFAULT_PROTOCOL_ERA } from "@inspector/core/mcp/types.js";
|
||||
import { isOAuthCapableServerType } from "@inspector/core/mcp/config.js";
|
||||
import type { Root } from "@modelcontextprotocol/client";
|
||||
|
||||
|
|
@ -57,6 +59,7 @@ export interface ServerSettingsFormProps {
|
|||
) => void;
|
||||
onAutoRefreshChange: (value: boolean) => void;
|
||||
onMaxFetchRequestsChange: (value: number) => void;
|
||||
onProtocolEraChange: (value: ServerProtocolEra) => void;
|
||||
onOAuthChange: (oauth: OAuthSettings) => void;
|
||||
onClearStoredOAuth?: () => void;
|
||||
onAddRoot: () => void;
|
||||
|
|
@ -64,6 +67,23 @@ export interface ServerSettingsFormProps {
|
|||
onRootChange: (index: number, uri: string, name: string) => void;
|
||||
}
|
||||
|
||||
// Protocol-era options (SEP §7.8). Values are `ServerProtocolEra`; the pinned
|
||||
// modern revision is an internal detail (MODERN_PROTOCOL_VERSION), so the label
|
||||
// stays version-free.
|
||||
const PROTOCOL_ERA_OPTIONS: { value: ServerProtocolEra; label: string }[] = [
|
||||
{ value: "legacy", label: "Legacy (2025-11-25 handshake)" },
|
||||
{ value: "auto", label: "Auto (probe, fall back to legacy)" },
|
||||
{ value: "modern", label: "Modern (2026-07-28, sessionless)" },
|
||||
];
|
||||
|
||||
const PROTOCOL_ERA_VALUES: ReadonlySet<ServerProtocolEra> = new Set(
|
||||
PROTOCOL_ERA_OPTIONS.map((o) => o.value),
|
||||
);
|
||||
|
||||
function isProtocolEra(value: string | null): value is ServerProtocolEra {
|
||||
return value !== null && PROTOCOL_ERA_VALUES.has(value as ServerProtocolEra);
|
||||
}
|
||||
|
||||
const RemoveIcon = ActionIcon.withProps({
|
||||
color: "red",
|
||||
variant: "subtle",
|
||||
|
|
@ -220,6 +240,7 @@ export function ServerSettingsForm({
|
|||
onTimeoutChange,
|
||||
onAutoRefreshChange,
|
||||
onMaxFetchRequestsChange,
|
||||
onProtocolEraChange,
|
||||
onOAuthChange,
|
||||
onClearStoredOAuth,
|
||||
onAddRoot,
|
||||
|
|
@ -273,6 +294,20 @@ export function ServerSettingsForm({
|
|||
<Accordion.Control>Options</Accordion.Control>
|
||||
<Accordion.Panel>
|
||||
<Stack gap="md">
|
||||
<Select
|
||||
label="Protocol Era"
|
||||
description="Which MCP protocol era to negotiate with this server (independent of the transport). Legacy uses the 2025-11-25 initialize handshake; Auto probes server/discover and falls back to legacy; Modern pins the 2026-07-28 sessionless protocol. Defaults to Legacy — debugging tools should not auto-probe."
|
||||
data={PROTOCOL_ERA_OPTIONS}
|
||||
value={settings.protocolEra ?? DEFAULT_PROTOCOL_ERA}
|
||||
onChange={(value) => {
|
||||
// Select emits `string | null`; the null (cleared) case is
|
||||
// unreachable here — the field is not clearable and the value
|
||||
// always resolves to a known era.
|
||||
/* v8 ignore next -- Select never emits an out-of-range value */
|
||||
if (isProtocolEra(value)) onProtocolEraChange(value);
|
||||
}}
|
||||
allowDeselect={false}
|
||||
/>
|
||||
<Checkbox
|
||||
label="Auto Refresh on List Changed Notifications"
|
||||
description="When checked, tool/prompt/resource lists refresh automatically when the server sends a */list_changed notification. When unchecked, the list-changed indicator appears and you refresh on demand."
|
||||
|
|
|
|||
Loading…
Reference in a new issue