Add InspectorView state machine driving screens from props
InspectorView replaces the prior pair (ConnectedView/UnconnectedView wrapping children) with a single state-machine view. Holds activeServer, connectionStatus, initializeResult, latencyMs, activeTab, and availableTabs internally; renders the appropriate screen based on activeTab; switches the header between connected and unconnected modes. Screen data (servers, tools, prompts, resources, resourceTemplates, subscriptions, logs, tasks, progressByTaskId, history) flows in via props so stories can populate the entire view with fixtures. Demo data lives in stories, not in the view. Stories include one server per transport (stdio, sse, streamable-http) so the TransportBadge / descriptor variants are all visible. Reorders ViewHeader.stories tabs to match the new ALL_TABS sequence. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
1149288ea6
commit
9553fb9c8a
3 changed files with 515 additions and 1 deletions
|
|
@ -31,8 +31,8 @@ export const Connected: Story = {
|
|||
"Tools",
|
||||
"Resources",
|
||||
"Prompts",
|
||||
"Logging",
|
||||
"Tasks",
|
||||
"Logging",
|
||||
"History",
|
||||
],
|
||||
onTabChange: fn(),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,285 @@
|
|||
import type {
|
||||
Prompt,
|
||||
Resource,
|
||||
ResourceTemplate,
|
||||
Task,
|
||||
Tool,
|
||||
} from "@modelcontextprotocol/sdk/types.js";
|
||||
import type {
|
||||
InspectorResourceSubscription,
|
||||
MessageEntry,
|
||||
} from "@inspector/core/mcp/types.js";
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { fn } from "storybook/test";
|
||||
import { InspectorView } from "./InspectorView";
|
||||
import type { ServerEntry } from "../../screens/ServerListScreen/ServerListScreen";
|
||||
import type { LogEntryData } from "../../elements/LogEntry/LogEntry";
|
||||
import type { TaskProgress } from "../../groups/TaskCard/TaskCard";
|
||||
|
||||
const demoServers: ServerEntry[] = [
|
||||
{
|
||||
id: "5e8c3d1f-2a4b-4c6d-8e7f-1a2b3c4d5e6f",
|
||||
name: "Local Dev Server",
|
||||
config: {
|
||||
command:
|
||||
"npx -y @modelcontextprotocol/server-filesystem /home/user/projects",
|
||||
},
|
||||
info: { name: "Local Dev Server", version: "1.2.0" },
|
||||
connection: { status: "disconnected" },
|
||||
},
|
||||
{
|
||||
id: "b3a7c1d2-9f8e-4a5b-bc6d-7e8f9a0b1c2d",
|
||||
name: "Legacy Events Server",
|
||||
config: { type: "sse", url: "https://legacy-events.example.com/mcp" },
|
||||
info: { name: "Legacy Events Server", version: "0.9.1" },
|
||||
connection: { status: "disconnected" },
|
||||
},
|
||||
{
|
||||
id: "c4d5e6f7-8a9b-4c0d-9e1f-2a3b4c5d6e7f",
|
||||
name: "Remote API Server",
|
||||
config: { type: "streamable-http", url: "https://api.example.com/mcp" },
|
||||
info: { name: "Remote API Server", version: "2.0.0" },
|
||||
connection: { status: "disconnected" },
|
||||
},
|
||||
];
|
||||
|
||||
const demoTools: Tool[] = [
|
||||
{
|
||||
name: "send_message",
|
||||
title: "Send Message",
|
||||
inputSchema: { type: "object" },
|
||||
},
|
||||
{
|
||||
name: "create_record",
|
||||
title: "Create Record",
|
||||
description: "Creates a new record with the given parameters",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
title: { type: "string", description: "Record title" },
|
||||
count: { type: "number", description: "Number of items" },
|
||||
enabled: {
|
||||
type: "boolean",
|
||||
description: "Whether the record is active",
|
||||
},
|
||||
},
|
||||
required: ["title"],
|
||||
},
|
||||
},
|
||||
{ name: "delete_records", inputSchema: { type: "object" } },
|
||||
{ name: "list_users", inputSchema: { type: "object" } },
|
||||
];
|
||||
|
||||
const demoPrompts: Prompt[] = [
|
||||
{
|
||||
name: "summarize",
|
||||
description: "Summarize the given text into key points",
|
||||
},
|
||||
{
|
||||
name: "translate",
|
||||
description: "Translate text from one language to another",
|
||||
arguments: [
|
||||
{ name: "text", required: true, description: "The text to translate" },
|
||||
{
|
||||
name: "targetLanguage",
|
||||
required: true,
|
||||
description: "Target language code",
|
||||
},
|
||||
],
|
||||
},
|
||||
{ name: "code-review", description: "Review code for issues" },
|
||||
{ name: "refactor" },
|
||||
];
|
||||
|
||||
const demoResources: Resource[] = [
|
||||
{
|
||||
name: "config.json",
|
||||
uri: "file:///config.json",
|
||||
annotations: { audience: ["user"], priority: 0.8 },
|
||||
},
|
||||
{ name: "README.md", uri: "file:///README.md" },
|
||||
{
|
||||
name: "schema.sql",
|
||||
uri: "file:///schema.sql",
|
||||
annotations: { priority: 0.5 },
|
||||
},
|
||||
{ name: "package.json", uri: "file:///package.json" },
|
||||
];
|
||||
|
||||
const demoResourceTemplates: ResourceTemplate[] = [
|
||||
{ name: "User Profile", uriTemplate: "file:///users/{userId}/profile" },
|
||||
{
|
||||
name: "Table Row",
|
||||
title: "Database Table Row",
|
||||
uriTemplate: "db://tables/{tableName}/rows/{rowId}",
|
||||
},
|
||||
];
|
||||
|
||||
const demoSubscriptions: InspectorResourceSubscription[] = [
|
||||
{
|
||||
resource: { name: "config.json", uri: "file:///config.json" },
|
||||
lastUpdated: new Date("2026-03-17T10:30:00Z"),
|
||||
},
|
||||
];
|
||||
|
||||
const demoLogs: LogEntryData[] = [
|
||||
{
|
||||
receivedAt: new Date("2026-03-17T10:00:00Z"),
|
||||
params: { level: "info", data: "Server started on port 3000" },
|
||||
},
|
||||
{
|
||||
receivedAt: new Date("2026-03-17T10:00:01Z"),
|
||||
params: {
|
||||
level: "debug",
|
||||
logger: "config",
|
||||
data: "Loading configuration from /etc/mcp/config.json",
|
||||
},
|
||||
},
|
||||
{
|
||||
receivedAt: new Date("2026-03-17T10:00:02Z"),
|
||||
params: {
|
||||
level: "warning",
|
||||
logger: "http",
|
||||
data: "Deprecated API endpoint called: /v1/tools",
|
||||
},
|
||||
},
|
||||
{
|
||||
receivedAt: new Date("2026-03-17T10:00:03Z"),
|
||||
params: {
|
||||
level: "error",
|
||||
logger: "resources",
|
||||
data: "Failed to read resource: file not found at /data/missing.txt",
|
||||
},
|
||||
},
|
||||
{
|
||||
receivedAt: new Date("2026-03-17T10:00:04Z"),
|
||||
params: {
|
||||
level: "info",
|
||||
logger: "tools",
|
||||
data: "Tool execution completed: search_files (245ms)",
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const demoTasks: Task[] = [
|
||||
{
|
||||
taskId: "d0b22eba71fa36229ce5c4dfadeaa7de",
|
||||
status: "working",
|
||||
ttl: 300000,
|
||||
createdAt: "2026-03-29T20:18:20Z",
|
||||
lastUpdatedAt: "2026-03-29T20:18:22Z",
|
||||
statusMessage: "Processing records 650 of 1000...",
|
||||
},
|
||||
{
|
||||
taskId: "d487b49aa39023d907b5a2a5b506cb3",
|
||||
status: "completed",
|
||||
ttl: 300000,
|
||||
createdAt: "2026-03-29T20:16:47Z",
|
||||
lastUpdatedAt: "2026-03-29T20:16:49Z",
|
||||
},
|
||||
{
|
||||
taskId: "e6ebffd9cca84ddd1646d3c579a4d453",
|
||||
status: "failed",
|
||||
ttl: 300000,
|
||||
createdAt: "2026-03-29T20:17:27Z",
|
||||
lastUpdatedAt: "2026-03-29T20:17:57Z",
|
||||
statusMessage: "Timeout waiting for tool response after 30s",
|
||||
},
|
||||
];
|
||||
|
||||
const demoProgressByTaskId: Record<string, TaskProgress> = {
|
||||
d0b22eba71fa36229ce5c4dfadeaa7de: {
|
||||
progress: 650,
|
||||
total: 1000,
|
||||
message: "Processing records 650 of 1000...",
|
||||
},
|
||||
};
|
||||
|
||||
const demoHistory: MessageEntry[] = [
|
||||
{
|
||||
id: "req-1",
|
||||
timestamp: new Date("2026-03-17T10:00:00Z"),
|
||||
direction: "request",
|
||||
message: {
|
||||
jsonrpc: "2.0",
|
||||
id: 1,
|
||||
method: "tools/call",
|
||||
params: {
|
||||
name: "send_message",
|
||||
arguments: { message: "Hello, world!" },
|
||||
},
|
||||
},
|
||||
response: {
|
||||
jsonrpc: "2.0",
|
||||
id: 1,
|
||||
result: {
|
||||
content: [{ type: "text", text: "Message sent successfully" }],
|
||||
},
|
||||
},
|
||||
duration: 120,
|
||||
},
|
||||
{
|
||||
id: "req-2",
|
||||
timestamp: new Date("2026-03-17T10:01:00Z"),
|
||||
direction: "request",
|
||||
message: {
|
||||
jsonrpc: "2.0",
|
||||
id: 2,
|
||||
method: "resources/read",
|
||||
params: { uri: "file:///config.json" },
|
||||
},
|
||||
response: {
|
||||
jsonrpc: "2.0",
|
||||
id: 2,
|
||||
result: { contents: [{ uri: "file:///config.json", text: "{}" }] },
|
||||
},
|
||||
duration: 45,
|
||||
},
|
||||
{
|
||||
id: "req-3",
|
||||
timestamp: new Date("2026-03-17T10:02:00Z"),
|
||||
direction: "request",
|
||||
message: {
|
||||
jsonrpc: "2.0",
|
||||
id: 3,
|
||||
method: "tools/call",
|
||||
params: { name: "delete_records", arguments: { ids: [1, 2, 3] } },
|
||||
},
|
||||
response: {
|
||||
jsonrpc: "2.0",
|
||||
id: 3,
|
||||
error: { code: -32000, message: "Permission denied" },
|
||||
},
|
||||
duration: 350,
|
||||
},
|
||||
];
|
||||
|
||||
const meta: Meta<typeof InspectorView> = {
|
||||
title: "Views/InspectorView",
|
||||
component: InspectorView,
|
||||
parameters: { layout: "fullscreen" },
|
||||
args: {
|
||||
onToggleTheme: fn(),
|
||||
servers: demoServers,
|
||||
tools: demoTools,
|
||||
prompts: demoPrompts,
|
||||
resources: demoResources,
|
||||
resourceTemplates: demoResourceTemplates,
|
||||
subscriptions: demoSubscriptions,
|
||||
logs: demoLogs,
|
||||
tasks: demoTasks,
|
||||
progressByTaskId: demoProgressByTaskId,
|
||||
history: demoHistory,
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof InspectorView>;
|
||||
|
||||
export const Default: Story = {};
|
||||
|
||||
export const NoServers: Story = {
|
||||
args: {
|
||||
servers: [],
|
||||
},
|
||||
};
|
||||
229
clients/web/src/components/views/InspectorView/InspectorView.tsx
Normal file
229
clients/web/src/components/views/InspectorView/InspectorView.tsx
Normal file
|
|
@ -0,0 +1,229 @@
|
|||
import { useMemo, useState } from "react";
|
||||
import { AppShell } from "@mantine/core";
|
||||
import type {
|
||||
InitializeResult,
|
||||
LoggingLevel,
|
||||
Prompt,
|
||||
Resource,
|
||||
ResourceTemplate,
|
||||
Task,
|
||||
Tool,
|
||||
} from "@modelcontextprotocol/sdk/types.js";
|
||||
import type {
|
||||
ConnectionStatus,
|
||||
InspectorResourceSubscription,
|
||||
MessageEntry,
|
||||
} from "@inspector/core/mcp/types.js";
|
||||
import { ViewHeader } from "../../groups/ViewHeader/ViewHeader";
|
||||
import {
|
||||
ServerListScreen,
|
||||
type ServerEntry,
|
||||
} from "../../screens/ServerListScreen/ServerListScreen";
|
||||
import { ToolsScreen } from "../../screens/ToolsScreen/ToolsScreen";
|
||||
import { PromptsScreen } from "../../screens/PromptsScreen/PromptsScreen";
|
||||
import { ResourcesScreen } from "../../screens/ResourcesScreen/ResourcesScreen";
|
||||
import { LoggingScreen } from "../../screens/LoggingScreen/LoggingScreen";
|
||||
import type { LogEntryData } from "../../elements/LogEntry/LogEntry";
|
||||
import { TasksScreen } from "../../screens/TasksScreen/TasksScreen";
|
||||
import type { TaskProgress } from "../../groups/TaskCard/TaskCard";
|
||||
import { HistoryScreen } from "../../screens/HistoryScreen/HistoryScreen";
|
||||
|
||||
const SERVERS_TAB = "Servers";
|
||||
|
||||
const ALL_TABS: string[] = [
|
||||
SERVERS_TAB,
|
||||
"Tools",
|
||||
"Prompts",
|
||||
"Resources",
|
||||
"Logs",
|
||||
"Tasks",
|
||||
"History",
|
||||
];
|
||||
|
||||
const noop = () => undefined;
|
||||
|
||||
export interface InspectorViewProps {
|
||||
servers: ServerEntry[];
|
||||
tools: Tool[];
|
||||
prompts: Prompt[];
|
||||
resources: Resource[];
|
||||
resourceTemplates: ResourceTemplate[];
|
||||
subscriptions: InspectorResourceSubscription[];
|
||||
logs: LogEntryData[];
|
||||
tasks: Task[];
|
||||
progressByTaskId?: Record<string, TaskProgress>;
|
||||
history: MessageEntry[];
|
||||
onToggleTheme: () => void;
|
||||
}
|
||||
|
||||
export function InspectorView({
|
||||
servers: serversInput,
|
||||
tools,
|
||||
prompts,
|
||||
resources,
|
||||
resourceTemplates,
|
||||
subscriptions,
|
||||
logs,
|
||||
tasks,
|
||||
progressByTaskId,
|
||||
history,
|
||||
onToggleTheme,
|
||||
}: InspectorViewProps) {
|
||||
const [activeServer, setActiveServer] = useState<string | undefined>(
|
||||
undefined,
|
||||
);
|
||||
const [initializeResult, setInitializeResult] = useState<
|
||||
InitializeResult | undefined
|
||||
>(undefined);
|
||||
const [connectionStatus, setConnectionStatus] =
|
||||
useState<ConnectionStatus>("disconnected");
|
||||
const [latencyMs, setLatencyMs] = useState<number | undefined>(undefined);
|
||||
const [activeTab, setActiveTab] = useState<string>(SERVERS_TAB);
|
||||
const [availableTabs, setAvailableTabs] = useState<string[]>([SERVERS_TAB]);
|
||||
const [logLevel, setLogLevel] = useState<LoggingLevel>("info");
|
||||
const [autoScroll, setAutoScroll] = useState<boolean>(true);
|
||||
|
||||
// Per-card connection state derives from "is this the active server,
|
||||
// and what's our current global connection status?". Cards that aren't
|
||||
// the active server always render as disconnected.
|
||||
const servers = useMemo<ServerEntry[]>(
|
||||
() =>
|
||||
serversInput.map((s) => ({
|
||||
...s,
|
||||
connection:
|
||||
s.id === activeServer
|
||||
? { status: connectionStatus }
|
||||
: { status: "disconnected" },
|
||||
})),
|
||||
[serversInput, activeServer, connectionStatus],
|
||||
);
|
||||
|
||||
function disconnect() {
|
||||
setActiveServer(undefined);
|
||||
setConnectionStatus("disconnected");
|
||||
setInitializeResult(undefined);
|
||||
setLatencyMs(undefined);
|
||||
setAvailableTabs([SERVERS_TAB]);
|
||||
setActiveTab(SERVERS_TAB);
|
||||
}
|
||||
|
||||
function handleToggleConnection(id: string) {
|
||||
if (id === activeServer && connectionStatus === "connected") {
|
||||
disconnect();
|
||||
return;
|
||||
}
|
||||
const target = serversInput.find((s) => s.id === id);
|
||||
if (!target) return;
|
||||
setActiveServer(id);
|
||||
setConnectionStatus("connected");
|
||||
setInitializeResult({
|
||||
protocolVersion: "2025-06-18",
|
||||
capabilities: {},
|
||||
serverInfo: target.info ?? { name: target.name, version: "0.0.0" },
|
||||
});
|
||||
setLatencyMs(42);
|
||||
setAvailableTabs(ALL_TABS);
|
||||
}
|
||||
|
||||
return (
|
||||
<AppShell header={{ height: 60 }} padding="md">
|
||||
<AppShell.Header>
|
||||
{connectionStatus === "connected" && initializeResult ? (
|
||||
<ViewHeader
|
||||
connected
|
||||
serverInfo={initializeResult.serverInfo}
|
||||
status={connectionStatus}
|
||||
latencyMs={latencyMs}
|
||||
activeTab={activeTab}
|
||||
availableTabs={availableTabs}
|
||||
onTabChange={setActiveTab}
|
||||
onDisconnect={disconnect}
|
||||
onToggleTheme={onToggleTheme}
|
||||
/>
|
||||
) : (
|
||||
<ViewHeader connected={false} onToggleTheme={onToggleTheme} />
|
||||
)}
|
||||
</AppShell.Header>
|
||||
<AppShell.Main>
|
||||
{activeTab === SERVERS_TAB && (
|
||||
<ServerListScreen
|
||||
servers={servers}
|
||||
activeServer={activeServer}
|
||||
onAddManually={noop}
|
||||
onImportConfig={noop}
|
||||
onImportServerJson={noop}
|
||||
onToggleConnection={handleToggleConnection}
|
||||
onServerInfo={noop}
|
||||
onSettings={noop}
|
||||
onEdit={noop}
|
||||
onClone={noop}
|
||||
onRemove={noop}
|
||||
/>
|
||||
)}
|
||||
{activeTab === "Tools" && (
|
||||
<ToolsScreen
|
||||
tools={tools}
|
||||
listChanged={false}
|
||||
onRefreshList={noop}
|
||||
onSelectTool={noop}
|
||||
onCallTool={noop}
|
||||
/>
|
||||
)}
|
||||
{activeTab === "Prompts" && (
|
||||
<PromptsScreen
|
||||
prompts={prompts}
|
||||
listChanged={false}
|
||||
onRefreshList={noop}
|
||||
onSelectPrompt={noop}
|
||||
onGetPrompt={noop}
|
||||
/>
|
||||
)}
|
||||
{activeTab === "Resources" && (
|
||||
<ResourcesScreen
|
||||
resources={resources}
|
||||
templates={resourceTemplates}
|
||||
subscriptions={subscriptions}
|
||||
listChanged={false}
|
||||
onRefreshList={noop}
|
||||
onSelectUri={noop}
|
||||
onSelectTemplate={noop}
|
||||
onReadResource={noop}
|
||||
onSubscribeResource={noop}
|
||||
onUnsubscribeResource={noop}
|
||||
/>
|
||||
)}
|
||||
{activeTab === "Logs" && (
|
||||
<LoggingScreen
|
||||
entries={logs}
|
||||
currentLevel={logLevel}
|
||||
onSetLevel={setLogLevel}
|
||||
onClear={noop}
|
||||
onExport={noop}
|
||||
autoScroll={autoScroll}
|
||||
onToggleAutoScroll={() => setAutoScroll((prev) => !prev)}
|
||||
onCopyAll={noop}
|
||||
/>
|
||||
)}
|
||||
{activeTab === "Tasks" && (
|
||||
<TasksScreen
|
||||
tasks={tasks}
|
||||
progressByTaskId={progressByTaskId}
|
||||
onRefresh={noop}
|
||||
onClearCompleted={noop}
|
||||
onCancel={noop}
|
||||
/>
|
||||
)}
|
||||
{activeTab === "History" && (
|
||||
<HistoryScreen
|
||||
entries={history}
|
||||
pinnedIds={new Set()}
|
||||
onClearAll={noop}
|
||||
onExport={noop}
|
||||
onReplay={noop}
|
||||
onTogglePin={noop}
|
||||
/>
|
||||
)}
|
||||
</AppShell.Main>
|
||||
</AppShell>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in a new issue