Merge branch 'v2/main' into v2/feat/2144-oauth-revocation

This commit is contained in:
Cliff Hall 2026-08-28 08:28:57 -04:00 • committed by GitHub
commit 5ba2fadd3b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 3326 additions and 1516 deletions

View file

@ -18,6 +18,20 @@ import type { Meta, StoryObj } from "@storybook/react-vite";
import { useState } from "react";
import { expect, fn } from "storybook/test";
import { InspectorView } from "./InspectorView";
import type {
AppsPanelProps,
ConnectionProps,
ConsolePanelProps,
LogsPanelProps,
NetworkPanelProps,
PromptsPanelProps,
ProtocolPanelProps,
ResourcesPanelProps,
ServerListProps,
ShellProps,
TasksPanelProps,
ToolsPanelProps,
} from "./types";
import { noopPagination } from "../../../test/fixtures/pagination";
import {
EMPTY_TOOLS_UI,
@ -324,116 +338,164 @@ const demoInitializeResult: InitializeResult = {
serverInfo: { name: "Local Dev Server", version: "1.2.0" },
};
// One constant per domain bundle, so a story can override a single field
// without replacing its whole bundle — Storybook merges args only at the top
// level, and each of these IS one top-level arg now (#2130).
const shellArgs: ShellProps = {
activeTab: "Servers",
onActiveTabChange: fn(),
onToggleTheme: fn(),
onOpenClientSettings: fn(),
};
// Stories default to "disconnected"; per-story overrides drive the connected /
// error narratives.
const connectionArgs: ConnectionProps = {
activeServer: undefined,
connectionStatus: "disconnected",
initializeResult: undefined,
latencyMs: undefined,
onToggleConnection: fn(),
onDisconnect: fn(),
};
// Callbacks are wired to storybook spies so play functions can assert on
// dispatch. Real wiring routes these to InspectorClient methods (the app shell
// at clients/web/src/App.tsx).
const serversArgs: ServerListProps = {
servers: demoServers,
onServerAdd: fn(),
onServerImportConfig: fn(),
onServerImportJson: fn(),
onServerExport: fn(),
onConnectionInfo: fn(),
onServerSettings: fn(),
onServerEdit: fn(),
onServerClone: fn(),
onServerRemove: fn(),
onServerReorder: fn(),
};
const toolsArgs: ToolsPanelProps = {
tools: demoTools,
toolsListChanged: false,
toolsUi: EMPTY_TOOLS_UI,
toolsPagination: noopPagination,
serverSupportsTaskToolCalls: false,
onToolsUiChange: fn(),
onCallTool: fn(),
onRefreshTools: fn(),
};
const promptsArgs: PromptsPanelProps = {
prompts: demoPrompts,
promptsListChanged: false,
promptsUi: EMPTY_PROMPTS_UI,
promptsPagination: noopPagination,
onPromptsUiChange: fn(),
onGetPrompt: fn(),
onRefreshPrompts: fn(),
};
const resourcesArgs: ResourcesPanelProps = {
resources: demoResources,
resourceTemplates: demoResourceTemplates,
subscriptions: demoSubscriptions,
resourcesListChanged: false,
resourcesUi: EMPTY_RESOURCES_UI,
resourcesPagination: noopPagination,
onResourcesUiChange: fn(),
onReadResource: fn(),
onSubscribeResource: fn(),
onUnsubscribeResource: fn(),
onRefreshResources: fn(),
};
const appsArgs: AppsPanelProps = {
appsUi: EMPTY_APPS_UI,
sandboxPath: "about:blank",
bridgeFactory: noopBridgeFactory,
appRendererRef: { current: null },
onAppsUiChange: fn(),
onSelectApp: fn(),
onOpenApp: fn(),
onCloseApp: fn(),
onAppError: fn(),
onRefreshApps: fn(),
};
const tasksArgs: TasksPanelProps = {
tasks: demoTasks,
progressByTaskId: demoProgressByTaskId,
tasksUi: EMPTY_TASKS_UI,
onTasksUiChange: fn(),
onCancelTask: fn(),
onClearCompletedTasks: fn(),
onRefreshTasks: fn(),
};
const logsArgs: LogsPanelProps = {
logs: demoLogs,
logsUi: EMPTY_LOGS_UI,
currentLogLevel: "info",
onSetLogLevel: fn(),
onLogsUiChange: fn(),
onClearLogs: fn(),
onExportLogs: fn(),
};
const protocolArgs: ProtocolPanelProps = {
protocol: demoHistory,
protocolUi: EMPTY_PROTOCOL_UI,
onProtocolUiChange: fn(),
onClearProtocol: fn(),
onExportProtocol: fn(),
onClearProtocolSection: fn(),
onExportProtocolSection: fn(),
onReplayProtocol: fn(),
onTogglePinProtocol: fn(),
};
const networkArgs: NetworkPanelProps = {
network: demoNetwork,
networkUi: EMPTY_NETWORK_UI,
onNetworkUiChange: fn(),
onClearNetwork: fn(),
onExportNetwork: fn(),
};
const consoleArgs: ConsolePanelProps = {
stderrLogs: demoStderr,
consoleUi: EMPTY_CONSOLE_UI,
onConsoleUiChange: fn(),
onClearConsole: fn(),
onExportConsole: fn(),
};
const meta: Meta<typeof InspectorView> = {
title: "Views/InspectorView",
component: InspectorView,
parameters: { layout: "fullscreen" },
args: {
// Data
servers: demoServers,
tools: demoTools,
prompts: demoPrompts,
resources: demoResources,
resourceTemplates: demoResourceTemplates,
subscriptions: demoSubscriptions,
logs: demoLogs,
tasks: demoTasks,
progressByTaskId: demoProgressByTaskId,
protocol: demoHistory,
network: demoNetwork,
stderrLogs: demoStderr,
// Connection state — stories default to "disconnected"; per-story
// overrides drive the connected / error narratives.
activeServer: undefined,
connectionStatus: "disconnected",
initializeResult: undefined,
latencyMs: undefined,
// Misc state
currentLogLevel: "info",
sandboxPath: "about:blank",
bridgeFactory: noopBridgeFactory,
appRendererRef: { current: null },
// Per-screen UI state (search / filter / selection), one object per screen.
toolsUi: EMPTY_TOOLS_UI,
promptsUi: EMPTY_PROMPTS_UI,
resourcesUi: EMPTY_RESOURCES_UI,
appsUi: EMPTY_APPS_UI,
tasksUi: EMPTY_TASKS_UI,
logsUi: EMPTY_LOGS_UI,
protocolUi: EMPTY_PROTOCOL_UI,
networkUi: EMPTY_NETWORK_UI,
consoleUi: EMPTY_CONSOLE_UI,
// Callbacks — all wired to storybook spies so play functions can assert
// on dispatch. Real wiring routes these to InspectorClient methods (the
// app shell at clients/web/src/App.tsx).
onToggleTheme: fn(),
onOpenClientSettings: fn(),
onToggleConnection: fn(),
onDisconnect: fn(),
onServerAdd: fn(),
onServerImportConfig: fn(),
onServerImportJson: fn(),
onServerExport: fn(),
onConnectionInfo: fn(),
onServerSettings: fn(),
onServerEdit: fn(),
onServerClone: fn(),
onServerRemove: fn(),
onServerReorder: fn(),
serverSupportsTaskToolCalls: false,
onToolsUiChange: fn(),
onCallTool: fn(),
onRefreshTools: fn(),
toolsPagination: noopPagination,
promptsPagination: noopPagination,
resourcesPagination: noopPagination,
onPromptsUiChange: fn(),
onGetPrompt: fn(),
onRefreshPrompts: fn(),
onResourcesUiChange: fn(),
onReadResource: fn(),
onSubscribeResource: fn(),
onUnsubscribeResource: fn(),
onRefreshResources: fn(),
onTasksUiChange: fn(),
onCancelTask: fn(),
onClearCompletedTasks: fn(),
onRefreshTasks: fn(),
onSetLogLevel: fn(),
onLogsUiChange: fn(),
onClearLogs: fn(),
onExportLogs: fn(),
onProtocolUiChange: fn(),
onClearProtocol: fn(),
onExportProtocol: fn(),
onReplayProtocol: fn(),
onTogglePinProtocol: fn(),
onNetworkUiChange: fn(),
onClearNetwork: fn(),
onExportNetwork: fn(),
onConsoleUiChange: fn(),
onClearConsole: fn(),
onExportConsole: fn(),
onAppsUiChange: fn(),
onSelectApp: fn(),
onOpenApp: fn(),
onCloseApp: fn(),
onAppError: fn(),
onRefreshApps: fn(),
activeTab: "Servers",
onActiveTabChange: fn(),
shell: shellArgs,
connection: connectionArgs,
servers: serversArgs,
tools: toolsArgs,
prompts: promptsArgs,
resources: resourcesArgs,
apps: appsArgs,
tasks: tasksArgs,
logs: logsArgs,
protocol: protocolArgs,
network: networkArgs,
console: consoleArgs,
},
render: (args) => {
const [activeTab, setActiveTab] = useState(args.activeTab ?? "Servers");
const [activeTab, setActiveTab] = useState(args.shell.activeTab);
return (
<InspectorView
{...args}
activeTab={activeTab}
onActiveTabChange={setActiveTab}
shell={{ ...args.shell, activeTab, onActiveTabChange: setActiveTab }}
/>
);
},
@ -463,7 +525,7 @@ export const Default: Story = {
export const NoServers: Story = {
args: {
servers: [],
servers: { ...serversArgs, servers: [] },
},
};
@ -480,7 +542,7 @@ const manyServers: ServerEntry[] = Array.from({ length: 24 }, (_, i) => ({
export const ManyServers: Story = {
args: {
servers: manyServers,
servers: { ...serversArgs, servers: manyServers },
},
play: async ({ canvasElement }) => {
// Even under enough content to overflow, the shell stays viewport-clamped
@ -509,10 +571,13 @@ export const ManyServers: Story = {
// regression / storybook play function coverage.
export const Connected: Story = {
args: {
activeServer: demoServers[0]!.id,
connectionStatus: "connected",
initializeResult: demoInitializeResult,
latencyMs: 142,
connection: {
...connectionArgs,
activeServer: demoServers[0]!.id,
connectionStatus: "connected",
initializeResult: demoInitializeResult,
latencyMs: 142,
},
},
};
@ -524,10 +589,13 @@ export const ConnectionError: Story = {
// connect-attempt-failure signal that gates the failure column; `network: []`
// (and empty Protocol) keep it a pure stdio failure so only Console is offered.
args: {
activeServer: demoServers[0]!.id,
erroredServerId: demoServers[0]!.id,
connectionStatus: "error",
network: [],
stderrLogs: demoStderr,
connection: {
...connectionArgs,
activeServer: demoServers[0]!.id,
erroredServerId: demoServers[0]!.id,
connectionStatus: "error",
},
network: { ...networkArgs, network: [] },
console: { ...consoleArgs, stderrLogs: demoStderr },
},
};

View file

@ -1,14 +1,4 @@
import {
useEffect,
useMemo,
useRef,
useState,
type ReactNode,
type Ref,
} from "react";
import type { MalformedListItem } from "@inspector/core/mcp";
import type { ReplayParamsOverride } from "../../../lib/protocolReplay";
import type { DeepLink, DeepLinkParseStatus } from "../../../utils/deepLink";
import { useEffect, useMemo, useRef, useState, type ReactNode } from "react";
import {
AppShell,
Flex,
@ -18,81 +8,38 @@ import {
type MantineTransition,
} from "@mantine/core";
import { useLocalStorage } from "@mantine/hooks";
import type {
Implementation,
InitializeResult,
LoggingLevel,
Prompt,
ProtocolEra,
ReadResourceResult,
Resource,
ResourceTemplateType as ResourceTemplate,
Task,
Tool,
} from "@modelcontextprotocol/client";
import type {
ConnectionStatus,
ExcludedTool,
FetchRequestEntry,
InspectorResourceSubscription,
MessageEntry,
ResourceSubscriptionStreamState,
ServerEntry,
StderrLogEntry,
} from "@inspector/core/mcp/types.js";
import type { Implementation, Tool } from "@modelcontextprotocol/client";
import type { ServerEntry } from "@inspector/core/mcp/types.js";
import { isTerminalStatus } from "@inspector/core/mcp/types.js";
import { isAppTool } from "@inspector/core/mcp/apps.js";
import { TASKS_EXTENSION_KEY } from "@inspector/core/mcp/modernTaskSchemas.js";
import { ViewHeader } from "../../groups/ViewHeader/ViewHeader";
import { VersionBadge } from "../../elements/VersionBadge/VersionBadge";
import { CopyrightBadge } from "../../elements/CopyrightBadge/CopyrightBadge";
import type { ListPaginationControlsProps } from "../../elements/ListPaginationControls/ListPaginationControls";
import { ServerListScreen } from "../../screens/ServerListScreen/ServerListScreen";
import {
ToolsScreen,
type ToolCallState,
type ToolsUiState,
} from "../../screens/ToolsScreen/ToolsScreen";
import {
AppsScreen,
type AppsUiState,
} from "../../screens/AppsScreen/AppsScreen";
import { ToolsScreen } from "../../screens/ToolsScreen/ToolsScreen";
import { AppsScreen } from "../../screens/AppsScreen/AppsScreen";
import { PromptsScreen } from "../../screens/PromptsScreen/PromptsScreen";
import { ResourcesScreen } from "../../screens/ResourcesScreen/ResourcesScreen";
import { LoggingScreen } from "../../screens/LoggingScreen/LoggingScreen";
import { TasksScreen } from "../../screens/TasksScreen/TasksScreen";
import { ProtocolScreen } from "../../screens/ProtocolScreen/ProtocolScreen";
import { NetworkScreen } from "../../screens/NetworkScreen/NetworkScreen";
import { ConsoleScreen } from "../../screens/ConsoleScreen/ConsoleScreen";
import type {
AppRendererHandle,
BridgeFactory,
} from "../../elements/AppRenderer/AppRenderer";
import {
PromptsScreen,
type GetPromptState,
type PromptsUiState,
} from "../../screens/PromptsScreen/PromptsScreen";
import {
ResourcesScreen,
type ReadResourceState,
type ResourcesUiState,
} from "../../screens/ResourcesScreen/ResourcesScreen";
import {
LoggingScreen,
type LogsUiState,
} from "../../screens/LoggingScreen/LoggingScreen";
import type { LogEntryData } from "../../elements/LogEntry/LogEntry";
import {
TasksScreen,
type TasksUiState,
} from "../../screens/TasksScreen/TasksScreen";
import type { TaskProgress } from "../../groups/TaskCard/TaskCard";
import {
ProtocolScreen,
type ProtocolUiState,
} from "../../screens/ProtocolScreen/ProtocolScreen";
import {
NetworkScreen,
type NetworkUiState,
} from "../../screens/NetworkScreen/NetworkScreen";
import {
ConsoleScreen,
type ConsoleUiState,
} from "../../screens/ConsoleScreen/ConsoleScreen";
AppsPanelProps,
ConnectionProps,
ConsolePanelProps,
LogsPanelProps,
NetworkPanelProps,
PromptsPanelProps,
ProtocolPanelProps,
ResourcesPanelProps,
ServerListProps,
ShellProps,
TasksPanelProps,
ToolsPanelProps,
} from "./types";
import type { SortDirection } from "../../elements/SortToggle/SortToggle";
import { ScreenStage } from "../../elements/ScreenStage/ScreenStage";
import { MonitoringScreen } from "../../groups/MonitoringScreen/MonitoringScreen";
@ -360,402 +307,193 @@ const MonitorColumnGroup = Group.withProps({
flex: "0 0 auto",
});
/**
* `InspectorView` takes one prop per domain rather than a wall of ~130 flat
* ones (#2130). Each bundle's shape lives in `./types`; the fields inside keep
* the names they had as flat props, so this is a regrouping rather than a
* rename.
*
* The bundles stop here: the function below destructures each one back into
* the same locals it already used, and the screens beneath receive the same
* individual props they always did.
*/
export interface InspectorViewProps {
/**
* Validated deep-link parameters from the page URL. When present and
* `openApp` is set, the parent switches to the Apps tab and pre-selects that
* app (with `appArgs` as the form values) once the connection is up and the
* app list contains it. The connect itself is driven by the parent.
*/
deepLink?: DeepLink;
/**
* Outcome of parsing the initial-URL deep link, surfaced as `data-deeplink`
* on the `connection-status` testid. Distinguishes "no deep link" from
* "rejected" (token mismatch / bad serverUrl) — both leave `data-status`
* idle, so an automated driver otherwise cannot tell them apart.
*/
deepLinkStatus?: DeepLinkParseStatus;
// Server list (static config; runtime connection state comes from the
// separate fields below and is merged into each card by this component).
servers: ServerEntry[];
/**
* Whether the server list is writable (catalog) or read-only (a `--config`
* session file / ad-hoc launch). When false, the Servers screen hides all
* catalog mutation controls. Defaults to true.
*/
serverListWritable?: boolean;
// Connection state — driven by the parent via `useInspectorClient`.
activeServer?: string;
/**
* Id of the server whose last connection attempt failed (#1621). Its card in
* the Servers screen draws a red border until another server is connected or
* a new connection is attempted. Independent of `activeServer`, which the
* parent clears on the failure's `disconnect` event.
*
* It is also the sole signal that opens the monitoring sidebar onto the
* failure's diagnostics (#2108). That used to be gated on the `"error"`
* connection status, which an OAuth failure never reaches — the parent drives
* that leg and tears the client down itself, settling the session at
* `"disconnected"`. So the parent must set this for *every* connect-attempt
* failure, auth legs included, and clear it as each new attempt starts.
*/
erroredServerId?: string;
/**
* Id of the server that just connected successfully (#1682). Its card draws
* the green highlight border and scrolls into view once the monitoring
* sidebar has opened — the success mirror of `erroredServerId`.
*/
connectedServerId?: string;
/**
* The Inspector build version (root `package.json`), shown at the left of the
* footer row (#1682). Absent on a legacy backend that omits it — the version
* label then renders nothing.
*/
version?: string;
connectionStatus: ConnectionStatus;
/**
* Last connection-level error message (handshake failure, OAuth start
* failure, deep-link automation failure). Surfaced as `data-error-message`
* on the header's `connection-status` testid so an automated driver can read
* *why* a connect failed without scraping a transient toast.
*/
connectErrorMessage?: string;
initializeResult?: InitializeResult;
latencyMs?: number;
// Primitive lists, log streams, task state — all sourced from the
// per-primitive `useManaged*` / `useMessageLog` hooks in the parent.
tools: Tool[];
/** Tools excluded from `tools/list` for invalid `x-mcp-header` annotations
* (SEP-2243); shown in the Tools sidebar with the reason (#1632). */
excludedTools?: ExcludedTool[];
/**
* Entries dropped from a list result as malformed, across every list method.
* Each list panel filters for its own and warns about them (#1909).
*/
malformedListItems?: MalformedListItem[];
prompts: Prompt[];
resources: Resource[];
resourceTemplates: ResourceTemplate[];
subscriptions: InspectorResourceSubscription[];
/**
* Modern-era `subscriptions/listen` stream state (#1630). Drives the
* Resources screen's stream badge/dot; `active: false` (or omitted) on the
* legacy era.
*/
subscriptionStreamState?: ResourceSubscriptionStreamState;
// "List changed since last refresh" flags, sourced from the managed-state
// layer (#1402). They light the per-screen list-changed indicator. Apps is a
// filtered view of tools, so it shares the tools flag.
toolsListChanged: boolean;
promptsListChanged: boolean;
resourcesListChanged: boolean;
// Last list-load failure per screen, sourced from the same managed-state
// layer. Rendered above the sidebar list so a failed load (including the
// connect-time one) can't read as "this server has none" (#1953).
toolsLoadError?: Error | null;
promptsLoadError?: Error | null;
/**
* The Resources sidebar lists resources AND templates behind a single
* Refresh, so App passes whichever of the two loads failed.
*/
resourcesLoadError?: Error | null;
logs: LogEntryData[];
tasks: Task[];
progressByTaskId?: Record<string, TaskProgress>;
protocol: MessageEntry[];
/** Negotiated protocol era (SEP §7.8), for the Protocol view's era badge. */
protocolEra?: ProtocolEra;
network: FetchRequestEntry[];
/** Captured stdio stderr (the Console screen). Empty for HTTP servers. (#1621) */
stderrLogs: StderrLogEntry[];
// Per-screen "operation in flight" states (panel-level; optional because
// the underlying screens accept them as optional).
toolCallState?: ToolCallState;
getPromptState?: GetPromptState;
readResourceState?: ReadResourceState;
// Per-screen selection / search / filter state, one object per screen. Owned
// by the parent (App) so it persists across tab navigation within a live
// session — the screens unmount on tab switch, so screen-local state would be
// lost (#1417). Each is paired with an `on{Screen}UiChange` setter below.
toolsUi: ToolsUiState;
promptsUi: PromptsUiState;
resourcesUi: ResourcesUiState;
appsUi: AppsUiState;
tasksUi: TasksUiState;
logsUi: LogsUiState;
protocolUi: ProtocolUiState;
networkUi: NetworkUiState;
consoleUi: ConsoleUiState;
/** Active inspector tab (lifted to App for OAuth resume). */
activeTab: string;
onActiveTabChange: (tab: string) => void;
// Logging level. The MCP `logging/setLevel` request has no echo
// notification, so the parent keeps the optimistic current value.
currentLogLevel: LoggingLevel;
// MCP Apps sandbox. The parent's web environment provides the sandbox iframe
// URL (undefined when the sandbox controller is unavailable), the per-app
// bridge factory, and the renderer handle the parent uses to push tool
// input/result into the running app and tear it down.
sandboxPath?: string;
bridgeFactory: BridgeFactory;
appRendererRef: Ref<AppRendererHandle>;
// Protocol pinning. Optional because pin state isn't persisted yet (#1244
// is single-PR; persistence is a separate concern).
pinnedProtocolIds?: Set<string>;
// Theme toggle (lives in the parent so the color scheme can also flow
// into other top-level UI later).
onToggleTheme: () => void;
/** Open install-level client settings (client.json / EMA IdP). */
onOpenClientSettings: () => void;
// Connection lifecycle (dispatched to `useInspectorClient.connect/disconnect`).
onToggleConnection: (id: string) => void;
onDisconnect: () => void;
// Server list actions.
onServerAdd: () => void;
onServerImportConfig: () => void;
onServerImportJson: () => void;
/** Download the current server list as a canonical `mcp.json` file. */
onServerExport: () => void;
onConnectionInfo: (id: string) => void;
onServerSettings: (id: string) => void;
onServerEdit: (id: string) => void;
onServerClone: (id: string) => void;
onServerRemove: (id: string) => void;
/** Persist a new server ordering (drag-and-drop / keyboard reorder). */
onServerReorder: (orderedIds: string[]) => void;
/** Ids of freshly-added servers to highlight on the list (first is scrolled to). */
highlightedServerIds?: string[];
/** Clears the freshly-added highlight for a server (on click of its card). */
onClearHighlight?: (id: string) => void;
// Per-primitive actions (route to `inspectorClient` methods / hook refresh).
// Each `on{Screen}UiChange` persists that screen's lifted UI state (#1417).
/** Whether the connected server advertises task-augmented tool calls. */
serverSupportsTaskToolCalls: boolean;
onToolsUiChange: (next: ToolsUiState) => void;
onCallTool: (
name: string,
args: Record<string, unknown>,
runAsTask?: boolean,
) => void;
onCancelToolCall?: () => void;
onClearToolResult?: () => void;
onRefreshTools: () => void;
/** Pagination controls for the Tools list (#1721). */
toolsPagination: ListPaginationControlsProps;
/** Pagination controls for the Prompts list (#1721). */
promptsPagination: ListPaginationControlsProps;
/** Pagination controls for the Resources list (#1721). */
resourcesPagination: ListPaginationControlsProps;
/**
* Read-on-demand handler for `resource_link` blocks in a tool result.
* Returns the linked resource's contents so the result panel can inline them.
*/
onReadResourceContents?: (uri: string) => Promise<ReadResourceResult>;
onPromptsUiChange: (next: PromptsUiState) => void;
onGetPrompt: (name: string, args: Record<string, string>) => void;
onCopyPromptMessages?: () => void;
onRefreshPrompts: () => void;
onResourcesUiChange: (next: ResourcesUiState) => void;
onReadResource: (uri: string) => void;
onSubscribeResource: (uri: string) => void;
onUnsubscribeResource: (uri: string) => void;
onRefreshResources: () => void;
onCompleteArgument?: (
ref:
| { type: "ref/resource"; uri: string }
| { type: "ref/prompt"; name: string },
argumentName: string,
argumentValue: string,
context: Record<string, string>,
) => Promise<string[]>;
completionsSupported?: boolean;
/**
* Whether the connected server advertises the `resources.subscribe`
* capability. When false, the Resources screen hides the Subscribe/
* Unsubscribe button and the Subscriptions accordion section.
*/
subscriptionsSupported?: boolean;
onTasksUiChange: (next: TasksUiState) => void;
onCancelTask: (taskId: string) => void;
onClearCompletedTasks: () => void;
onRefreshTasks: () => void;
onSetLogLevel: (level: LoggingLevel) => void;
/**
* Modern-era per-request log level currently stamped, or `null` when opted
* out (#1629). On modern connections the Logs sidebar shows a per-request
* opt-in control instead of the legacy `logging/setLevel` selector.
*/
modernLogLevel?: LoggingLevel | null;
/** Set (or clear, with `null`) the modern per-request log level. */
onSetModernLogLevel?: (level: LoggingLevel | null) => void;
onLogsUiChange: (next: LogsUiState) => void;
onClearLogs: () => void;
onExportLogs: () => void;
onProtocolUiChange: (next: ProtocolUiState) => void;
onClearProtocol: () => void;
onExportProtocol: () => void;
onClearProtocolSection: (section: "pinned" | "history") => void;
onExportProtocolSection: (section: "pinned" | "history") => void;
onReplayProtocol: (id: string, overrideParams?: ReplayParamsOverride) => void;
onTogglePinProtocol: (id: string) => void;
onNetworkUiChange: (next: NetworkUiState) => void;
onClearNetwork: () => void;
onExportNetwork: () => void;
onConsoleUiChange: (next: ConsoleUiState) => void;
onClearConsole: () => void;
onExportConsole: () => void;
onAppsUiChange: (next: AppsUiState) => void;
onSelectApp: (name: string) => void;
onOpenApp: (name: string, args: Record<string, unknown>) => void;
onCloseApp: () => void;
onAppError: (err: Error) => void;
onRefreshApps: () => void;
/** App chrome: active tab, theme/settings entry points, footer, deep link. */
shell: ShellProps;
/** Live connection state plus the capability facts several screens read. */
connection: ConnectionProps;
/** The server catalog and every action on it. */
servers: ServerListProps;
tools: ToolsPanelProps;
prompts: PromptsPanelProps;
resources: ResourcesPanelProps;
apps: AppsPanelProps;
tasks: TasksPanelProps;
logs: LogsPanelProps;
protocol: ProtocolPanelProps;
network: NetworkPanelProps;
console: ConsolePanelProps;
}
export function InspectorView({
deepLink,
deepLinkStatus,
servers: serversInput,
serverListWritable = true,
activeServer,
erroredServerId,
connectedServerId,
version,
connectionStatus,
connectErrorMessage,
initializeResult,
latencyMs,
tools,
excludedTools = [],
malformedListItems = [],
prompts,
resources,
resourceTemplates,
toolsListChanged,
promptsListChanged,
resourcesListChanged,
toolsLoadError,
promptsLoadError,
resourcesLoadError,
subscriptions,
subscriptionStreamState,
logs,
tasks,
progressByTaskId,
protocol,
protocolEra,
network,
stderrLogs,
toolCallState,
getPromptState,
readResourceState,
toolsUi,
promptsUi,
resourcesUi,
appsUi,
tasksUi,
logsUi,
protocolUi,
networkUi,
consoleUi,
currentLogLevel,
sandboxPath,
bridgeFactory,
appRendererRef,
pinnedProtocolIds,
onToggleTheme,
onOpenClientSettings,
onToggleConnection,
onDisconnect,
onServerAdd,
onServerImportConfig,
onServerImportJson,
onServerExport,
onConnectionInfo,
onServerSettings,
onServerEdit,
onServerClone,
onServerRemove,
onServerReorder,
highlightedServerIds,
onClearHighlight,
serverSupportsTaskToolCalls,
onToolsUiChange,
onCallTool,
onCancelToolCall,
onClearToolResult,
onReadResourceContents,
onRefreshTools,
toolsPagination,
promptsPagination,
resourcesPagination,
onPromptsUiChange,
onGetPrompt,
onCopyPromptMessages,
onRefreshPrompts,
onResourcesUiChange,
onReadResource,
onSubscribeResource,
onUnsubscribeResource,
onRefreshResources,
onCompleteArgument,
completionsSupported,
subscriptionsSupported,
onTasksUiChange,
onCancelTask,
onClearCompletedTasks,
onRefreshTasks,
onSetLogLevel,
modernLogLevel = null,
onSetModernLogLevel,
onLogsUiChange,
onClearLogs,
onExportLogs,
onProtocolUiChange,
onClearProtocol,
onExportProtocol,
onClearProtocolSection,
onExportProtocolSection,
onReplayProtocol,
onTogglePinProtocol,
onNetworkUiChange,
onClearNetwork,
onExportNetwork,
onConsoleUiChange,
onClearConsole,
onExportConsole,
onAppsUiChange,
onSelectApp,
onOpenApp,
onCloseApp,
onAppError,
onRefreshApps,
activeTab: activeTabProp,
onActiveTabChange,
shell,
connection,
servers: serverList,
tools: toolsPanel,
prompts: promptsPanel,
resources: resourcesPanel,
apps: appsPanel,
tasks: tasksPanel,
logs: logsPanel,
protocol: protocolPanel,
network: networkPanel,
console: consolePanel,
}: InspectorViewProps) {
// Unpack the bundles back into the locals the body already uses, so the
// grouping is confined to the prop boundary.
const {
deepLink,
deepLinkStatus,
version,
malformedListItems = [],
activeTab: activeTabProp,
onActiveTabChange,
onToggleTheme,
onOpenClientSettings,
} = shell;
const {
activeServer,
erroredServerId,
connectedServerId,
connectionStatus,
connectErrorMessage,
initializeResult,
latencyMs,
protocolEra,
onCompleteArgument,
completionsSupported,
onToggleConnection,
onDisconnect,
} = connection;
const {
servers: serversInput,
serverListWritable = true,
highlightedServerIds,
onClearHighlight,
onServerAdd,
onServerImportConfig,
onServerImportJson,
onServerExport,
onConnectionInfo,
onServerSettings,
onServerEdit,
onServerClone,
onServerRemove,
onServerReorder,
} = serverList;
const {
tools,
excludedTools = [],
toolsListChanged,
toolsLoadError,
toolsUi,
toolCallState,
toolsPagination,
serverSupportsTaskToolCalls,
onToolsUiChange,
onCallTool,
onCancelToolCall,
onClearToolResult,
onRefreshTools,
onReadResourceContents,
} = toolsPanel;
const {
prompts,
promptsListChanged,
promptsLoadError,
promptsUi,
getPromptState,
promptsPagination,
onPromptsUiChange,
onGetPrompt,
onCopyPromptMessages,
onRefreshPrompts,
} = promptsPanel;
const {
resources,
resourceTemplates,
subscriptions,
subscriptionStreamState,
subscriptionsSupported,
resourcesListChanged,
resourcesLoadError,
resourcesUi,
readResourceState,
resourcesPagination,
onResourcesUiChange,
onReadResource,
onSubscribeResource,
onUnsubscribeResource,
onRefreshResources,
} = resourcesPanel;
const {
appsUi,
sandboxPath,
bridgeFactory,
appRendererRef,
onAppsUiChange,
onSelectApp,
onOpenApp,
onCloseApp,
onAppError,
onRefreshApps,
} = appsPanel;
const {
tasks,
progressByTaskId,
tasksUi,
onTasksUiChange,
onCancelTask,
onClearCompletedTasks,
onRefreshTasks,
} = tasksPanel;
const {
logs,
logsUi,
currentLogLevel,
modernLogLevel = null,
onSetLogLevel,
onSetModernLogLevel,
onLogsUiChange,
onClearLogs,
onExportLogs,
} = logsPanel;
const {
protocol,
protocolUi,
pinnedProtocolIds,
onProtocolUiChange,
onClearProtocol,
onExportProtocol,
onClearProtocolSection,
onExportProtocolSection,
onReplayProtocol,
onTogglePinProtocol,
} = protocolPanel;
const {
network,
networkUi,
onNetworkUiChange,
onClearNetwork,
onExportNetwork,
} = networkPanel;
const {
stderrLogs,
consoleUi,
onConsoleUiChange,
onClearConsole,
onExportConsole,
} = consolePanel;
// UI-only state. Connection state, primitive lists, and all action
// dispatching live in the parent; this component only owns view-local
// toggles (sort direction, list compact). Tab selection is lifted (#1417).

View file

@ -0,0 +1,379 @@
// Domain prop bundles for `InspectorView` (#2130).
//
// The view used to take ~130 flat props, which made App.tsx's JSX a prop wall
// rather than a component tree. Each bundle below groups one domain's data,
// UI state, and actions into a single object, so the call site reads as the
// tree it is and a hook can hand over exactly one bundle.
//
// Two conventions keep the change reviewable and keep it from rippling
// downward:
//
// - **Field names are unchanged.** Every field carries the exact name it had
// as a flat prop, so the bundles are a regrouping, not a rename.
// - **The bundles stop here.** `InspectorView` destructures them back into
// the same locals and passes them down to the screens as before, so
// nothing below the view knows they exist.
import type { Ref } from "react";
import type {
InitializeResult,
LoggingLevel,
Prompt,
ProtocolEra,
ReadResourceResult,
Resource,
ResourceTemplateType as ResourceTemplate,
Task,
Tool,
} from "@modelcontextprotocol/client";
import type { MalformedListItem } from "@inspector/core/mcp";
import type {
ConnectionStatus,
ExcludedTool,
FetchRequestEntry,
InspectorResourceSubscription,
MessageEntry,
ResourceSubscriptionStreamState,
ServerEntry,
StderrLogEntry,
} from "@inspector/core/mcp/types.js";
import type { ReplayParamsOverride } from "../../../lib/protocolReplay";
import type { DeepLink, DeepLinkParseStatus } from "../../../utils/deepLink";
import type { ListPaginationControlsProps } from "../../elements/ListPaginationControls/ListPaginationControls";
import type {
AppRendererHandle,
BridgeFactory,
} from "../../elements/AppRenderer/AppRenderer";
import type { LogEntryData } from "../../elements/LogEntry/LogEntry";
import type { TaskProgress } from "../../groups/TaskCard/TaskCard";
import type {
ToolCallState,
ToolsUiState,
} from "../../screens/ToolsScreen/ToolsScreen";
import type { AppsUiState } from "../../screens/AppsScreen/AppsScreen";
import type {
GetPromptState,
PromptsUiState,
} from "../../screens/PromptsScreen/PromptsScreen";
import type {
ReadResourceState,
ResourcesUiState,
} from "../../screens/ResourcesScreen/ResourcesScreen";
import type { LogsUiState } from "../../screens/LoggingScreen/LoggingScreen";
import type { TasksUiState } from "../../screens/TasksScreen/TasksScreen";
import type { ProtocolUiState } from "../../screens/ProtocolScreen/ProtocolScreen";
import type { NetworkUiState } from "../../screens/NetworkScreen/NetworkScreen";
import type { ConsoleUiState } from "../../screens/ConsoleScreen/ConsoleScreen";
/**
* App chrome and the few things that belong to no single screen: the active
* tab, the theme/client-settings entry points, the footer version, the
* deep-link parameters, and the malformed-list-item log that three separate
* screens each filter for their own entries.
*/
export interface ShellProps {
/**
* Validated deep-link parameters from the page URL. When present and
* `openApp` is set, the parent switches to the Apps tab and pre-selects that
* app (with `appArgs` as the form values) once the connection is up and the
* app list contains it. The connect itself is driven by the parent.
*/
deepLink?: DeepLink;
/**
* Outcome of parsing the initial-URL deep link, surfaced as `data-deeplink`
* on the `connection-status` testid. Distinguishes "no deep link" from
* "rejected" (token mismatch / bad serverUrl) — both leave `data-status`
* idle, so an automated driver otherwise cannot tell them apart.
*/
deepLinkStatus?: DeepLinkParseStatus;
/**
* The Inspector build version (root `package.json`), shown at the left of the
* footer row (#1682). Absent on a legacy backend that omits it — the version
* label then renders nothing.
*/
version?: string;
/**
* Entries dropped from a list result as malformed, across every list method.
* Each list panel filters for its own and warns about them (#1909). It spans
* three screens, so it has no single domain owner.
*/
malformedListItems?: MalformedListItem[];
/** Active inspector tab (lifted to App for OAuth resume). */
activeTab: string;
onActiveTabChange: (tab: string) => void;
/**
* Theme toggle (lives in the parent so the color scheme can also flow into
* other top-level UI later).
*/
onToggleTheme: () => void;
/** Open install-level client settings (client.json / EMA IdP). */
onOpenClientSettings: () => void;
}
/**
* Live connection state and the capability facts more than one screen reads.
* Driven by the parent via `useInspectorClient`.
*/
export interface ConnectionProps {
activeServer?: string;
/**
* Id of the server whose last connection attempt failed (#1621). Its card in
* the Servers screen draws a red border until another server is connected or
* a new connection is attempted. Independent of `activeServer`, which the
* parent clears on the failure's `disconnect` event.
*
* It is also the sole signal that opens the monitoring sidebar onto the
* failure's diagnostics (#2108). That used to be gated on the `"error"`
* connection status, which an OAuth failure never reaches — the parent drives
* that leg and tears the client down itself, settling the session at
* `"disconnected"`. So the parent must set this for *every* connect-attempt
* failure, auth legs included, and clear it as each new attempt starts.
*/
erroredServerId?: string;
/**
* Id of the server that just connected successfully (#1682). Its card draws
* the green highlight border and scrolls into view once the monitoring
* sidebar has opened — the success mirror of `erroredServerId`.
*/
connectedServerId?: string;
connectionStatus: ConnectionStatus;
/**
* Last connection-level error message (handshake failure, OAuth start
* failure, deep-link automation failure). Surfaced as `data-error-message`
* on the header's `connection-status` testid so an automated driver can read
* *why* a connect failed without scraping a transient toast.
*/
connectErrorMessage?: string;
initializeResult?: InitializeResult;
latencyMs?: number;
/** Negotiated protocol era (SEP §7.8), for the Protocol view's era badge. */
protocolEra?: ProtocolEra;
/**
* Argument completion, shared by the Prompts and Resources screens — hence
* here rather than in either one.
*/
onCompleteArgument?: (
ref:
| { type: "ref/resource"; uri: string }
| { type: "ref/prompt"; name: string },
argumentName: string,
argumentValue: string,
context: Record<string, string>,
) => Promise<string[]>;
completionsSupported?: boolean;
// Connection lifecycle (dispatched to `useInspectorClient.connect/disconnect`).
onToggleConnection: (id: string) => void;
onDisconnect: () => void;
}
/**
* The server catalog and every action on it. Static config only — the runtime
* connection state comes from {@link ConnectionProps} and is merged into each
* card by the view.
*/
export interface ServerListProps {
servers: ServerEntry[];
/**
* Whether the server list is writable (catalog) or read-only (a `--config`
* session file / ad-hoc launch). When false, the Servers screen hides all
* catalog mutation controls. Defaults to true.
*/
serverListWritable?: boolean;
/** Ids of freshly-added servers to highlight on the list (first is scrolled to). */
highlightedServerIds?: string[];
/** Clears the freshly-added highlight for a server (on click of its card). */
onClearHighlight?: (id: string) => void;
onServerAdd: () => void;
onServerImportConfig: () => void;
onServerImportJson: () => void;
/** Download the current server list as a canonical `mcp.json` file. */
onServerExport: () => void;
onConnectionInfo: (id: string) => void;
onServerSettings: (id: string) => void;
onServerEdit: (id: string) => void;
onServerClone: (id: string) => void;
onServerRemove: (id: string) => void;
/** Persist a new server ordering (drag-and-drop / keyboard reorder). */
onServerReorder: (orderedIds: string[]) => void;
}
/** The Tools screen: its list, lifted UI state, and actions. */
export interface ToolsPanelProps {
tools: Tool[];
/**
* Tools excluded from `tools/list` for invalid `x-mcp-header` annotations
* (SEP-2243); shown in the Tools sidebar with the reason (#1632).
*/
excludedTools?: ExcludedTool[];
/** "List changed since last refresh", from the managed-state layer (#1402). */
toolsListChanged: boolean;
/**
* Last list-load failure, rendered above the sidebar list so a failed load
* (including the connect-time one) can't read as "this server has none"
* (#1953).
*/
toolsLoadError?: Error | null;
/**
* Lifted selection / search / filter state. Owned by the parent so it
* persists across tab navigation within a live session — the screens unmount
* on tab switch, so screen-local state would be lost (#1417).
*/
toolsUi: ToolsUiState;
/** Panel-level "operation in flight" state. */
toolCallState?: ToolCallState;
/** Pagination controls for the Tools list (#1721). */
toolsPagination: ListPaginationControlsProps;
/** Whether the connected server advertises task-augmented tool calls. */
serverSupportsTaskToolCalls: boolean;
onToolsUiChange: (next: ToolsUiState) => void;
onCallTool: (
name: string,
args: Record<string, unknown>,
runAsTask?: boolean,
) => void;
onCancelToolCall?: () => void;
onClearToolResult?: () => void;
onRefreshTools: () => void;
/**
* Read-on-demand handler for `resource_link` blocks in a tool result.
* Returns the linked resource's contents so the result panel can inline them.
*/
onReadResourceContents?: (uri: string) => Promise<ReadResourceResult>;
}
/** The Prompts screen: its list, lifted UI state, and actions. */
export interface PromptsPanelProps {
prompts: Prompt[];
promptsListChanged: boolean;
promptsLoadError?: Error | null;
promptsUi: PromptsUiState;
getPromptState?: GetPromptState;
/** Pagination controls for the Prompts list (#1721). */
promptsPagination: ListPaginationControlsProps;
onPromptsUiChange: (next: PromptsUiState) => void;
onGetPrompt: (name: string, args: Record<string, string>) => void;
onCopyPromptMessages?: () => void;
onRefreshPrompts: () => void;
}
/** The Resources screen: resources, templates, subscriptions, and actions. */
export interface ResourcesPanelProps {
resources: Resource[];
resourceTemplates: ResourceTemplate[];
subscriptions: InspectorResourceSubscription[];
/**
* Modern-era `subscriptions/listen` stream state (#1630). Drives the
* Resources screen's stream badge/dot; `active: false` (or omitted) on the
* legacy era.
*/
subscriptionStreamState?: ResourceSubscriptionStreamState;
/**
* Whether the connected server advertises the `resources.subscribe`
* capability. When false, the Resources screen hides the Subscribe/
* Unsubscribe button and the Subscriptions accordion section.
*/
subscriptionsSupported?: boolean;
resourcesListChanged: boolean;
/**
* The Resources sidebar lists resources AND templates behind a single
* Refresh, so App passes whichever of the two loads failed.
*/
resourcesLoadError?: Error | null;
resourcesUi: ResourcesUiState;
readResourceState?: ReadResourceState;
/** Pagination controls for the Resources list (#1721). */
resourcesPagination: ListPaginationControlsProps;
onResourcesUiChange: (next: ResourcesUiState) => void;
onReadResource: (uri: string) => void;
onSubscribeResource: (uri: string) => void;
onUnsubscribeResource: (uri: string) => void;
onRefreshResources: () => void;
}
/**
* The Apps screen and its sandbox wiring. The parent's web environment
* provides the sandbox iframe URL (undefined when the sandbox controller is
* unavailable), the per-app bridge factory, and the renderer handle the parent
* uses to push tool input/result into the running app and tear it down.
*/
export interface AppsPanelProps {
appsUi: AppsUiState;
sandboxPath?: string;
bridgeFactory: BridgeFactory;
appRendererRef: Ref<AppRendererHandle>;
onAppsUiChange: (next: AppsUiState) => void;
onSelectApp: (name: string) => void;
onOpenApp: (name: string, args: Record<string, unknown>) => void;
onCloseApp: () => void;
onAppError: (err: Error) => void;
onRefreshApps: () => void;
}
/** The Tasks monitor: the task list, its progress map, and actions. */
export interface TasksPanelProps {
tasks: Task[];
progressByTaskId?: Record<string, TaskProgress>;
tasksUi: TasksUiState;
onTasksUiChange: (next: TasksUiState) => void;
onCancelTask: (taskId: string) => void;
onClearCompletedTasks: () => void;
onRefreshTasks: () => void;
}
/** The Logs monitor, including both eras' log-level controls. */
export interface LogsPanelProps {
logs: LogEntryData[];
logsUi: LogsUiState;
/**
* The MCP `logging/setLevel` request has no echo notification, so the parent
* keeps the optimistic current value.
*/
currentLogLevel: LoggingLevel;
/**
* Modern-era per-request log level currently stamped, or `null` when opted
* out (#1629). On modern connections the Logs sidebar shows a per-request
* opt-in control instead of the legacy `logging/setLevel` selector.
*/
modernLogLevel?: LoggingLevel | null;
onSetLogLevel: (level: LoggingLevel) => void;
/** Set (or clear, with `null`) the modern per-request log level. */
onSetModernLogLevel?: (level: LoggingLevel | null) => void;
onLogsUiChange: (next: LogsUiState) => void;
onClearLogs: () => void;
onExportLogs: () => void;
}
/** The Protocol monitor: the message log, pinning, replay, and exports. */
export interface ProtocolPanelProps {
protocol: MessageEntry[];
protocolUi: ProtocolUiState;
/**
* Pinned message ids. Optional because pin state isn't persisted yet (#1244
* is single-PR; persistence is a separate concern).
*/
pinnedProtocolIds?: Set<string>;
onProtocolUiChange: (next: ProtocolUiState) => void;
onClearProtocol: () => void;
onExportProtocol: () => void;
onClearProtocolSection: (section: "pinned" | "history") => void;
onExportProtocolSection: (section: "pinned" | "history") => void;
onReplayProtocol: (id: string, overrideParams?: ReplayParamsOverride) => void;
onTogglePinProtocol: (id: string) => void;
}
/** The Network monitor: captured HTTP traffic and its actions. */
export interface NetworkPanelProps {
network: FetchRequestEntry[];
networkUi: NetworkUiState;
onNetworkUiChange: (next: NetworkUiState) => void;
onClearNetwork: () => void;
onExportNetwork: () => void;
}
/** The Console monitor: captured stdio stderr. Empty for HTTP servers (#1621). */
export interface ConsolePanelProps {
stderrLogs: StderrLogEntry[];
consoleUi: ConsoleUiState;
onConsoleUiChange: (next: ConsoleUiState) => void;
onClearConsole: () => void;
onExportConsole: () => void;
}