Merge pull request #1221 from modelcontextprotocol/v2/phase-2-server-settings-viewshell

Phase 2: Refactor Server/Settings/ViewShell cluster to MCP SDK types
This commit is contained in:
Cliff Hall 2026-04-25 19:56:35 -04:00 • committed by GitHub
commit d9b2009804
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
32 changed files with 1790 additions and 1913 deletions

View file

@ -1,41 +1,59 @@
import { import {
Title,
Text,
Container,
Stack,
useMantineColorScheme,
useComputedColorScheme,
ActionIcon, ActionIcon,
Container,
Group, Group,
Stack,
Text,
Title,
useComputedColorScheme,
useMantineColorScheme,
} from "@mantine/core"; } from "@mantine/core";
import { MdDarkMode, MdLightMode } from "react-icons/md";
const PageContainer = Container.withProps({
size: "sm",
py: "xl",
});
const PageStack = Stack.withProps({
align: "center",
gap: "md",
});
const ThemeToggle = ActionIcon.withProps({
variant: "default",
size: "lg",
"aria-label": "Toggle color scheme",
});
const SubtitleText = Text.withProps({
c: "var(--inspector-text-secondary)",
});
function App() { function App() {
const { setColorScheme } = useMantineColorScheme(); const { setColorScheme } = useMantineColorScheme();
const computedColorScheme = useComputedColorScheme("light"); const computedColorScheme = useComputedColorScheme("light");
const isDark = computedColorScheme === "dark";
const ThemeIcon = isDark ? MdLightMode : MdDarkMode;
const toggleColorScheme = () => { function toggleColorScheme() {
setColorScheme(computedColorScheme === "dark" ? "light" : "dark"); setColorScheme(isDark ? "light" : "dark");
}; }
return ( return (
<Container size="sm" py="xl"> <PageContainer>
<Stack align="center" gap="md"> <PageStack>
<Group> <Group>
<Title order={1}>MCP Inspector</Title> <Title order={1}>MCP Inspector</Title>
<ActionIcon <ThemeToggle onClick={toggleColorScheme}>
onClick={toggleColorScheme} <ThemeIcon size={20} />
variant="default" </ThemeToggle>
size="lg"
aria-label="Toggle color scheme"
>
{computedColorScheme === "dark" ? "\u2600" : "\u263E"}
</ActionIcon>
</Group> </Group>
<Text c="var(--inspector-text-secondary)"> <SubtitleText>
Web client for the Model Context Protocol Inspector Web client for the Model Context Protocol Inspector
</Text> </SubtitleText>
</Stack> </PageStack>
</Container> </PageContainer>
); );
} }

View file

@ -6,8 +6,7 @@ const meta: Meta<typeof ConnectionToggle> = {
title: "Elements/ConnectionToggle", title: "Elements/ConnectionToggle",
component: ConnectionToggle, component: ConnectionToggle,
args: { args: {
onConnect: fn(), onToggle: fn(),
onDisconnect: fn(),
}, },
}; };

View file

@ -4,15 +4,13 @@ import type { ConnectionStatus } from "@inspector/core/mcp/types.js";
export interface ConnectionToggleProps { export interface ConnectionToggleProps {
status: ConnectionStatus; status: ConnectionStatus;
disabled?: boolean; disabled?: boolean;
onConnect: () => void; onToggle: () => void;
onDisconnect: () => void;
} }
export function ConnectionToggle({ export function ConnectionToggle({
status, status,
disabled = false, disabled = false,
onConnect, onToggle,
onDisconnect,
}: ConnectionToggleProps) { }: ConnectionToggleProps) {
const isConnected = status === "connected"; const isConnected = status === "connected";
const isConnecting = status === "connecting"; const isConnecting = status === "connecting";
@ -22,13 +20,7 @@ export function ConnectionToggle({
size="lg" size="lg"
checked={isConnected || isConnecting} checked={isConnected || isConnecting}
disabled={disabled || isConnecting} disabled={disabled || isConnecting}
onChange={(event) => { onChange={onToggle}
if (event.currentTarget.checked) {
onConnect();
} else {
onDisconnect();
}
}}
/> />
); );
} }

View file

@ -1,10 +1,15 @@
import { Group, Paper, Text } from "@mantine/core"; import { Group, Paper, Text } from "@mantine/core";
import { useMediaQuery } from "@mantine/hooks";
import type { ConnectionStatus } from "@inspector/core/mcp/types.js"; import type { ConnectionStatus } from "@inspector/core/mcp/types.js";
export interface ServerStatusIndicatorProps { export interface ServerStatusIndicatorProps {
status: ConnectionStatus; status: ConnectionStatus;
latencyMs?: number; latencyMs?: number;
retryCount?: number; retryCount?: number;
// Override for the default viewport-based label visibility. Useful when
// the indicator renders inside a constrained container (sidebar, card)
// where the global media query doesn't reflect available space.
showLabel?: boolean;
} }
const statusColorVar: Record<ConnectionStatus, string> = { const statusColorVar: Record<ConnectionStatus, string> = {
@ -41,14 +46,19 @@ export function ServerStatusIndicator({
status, status,
latencyMs, latencyMs,
retryCount, retryCount,
showLabel: showLabelProp,
}: ServerStatusIndicatorProps) { }: ServerStatusIndicatorProps) {
const wideViewport = useMediaQuery("(min-width: 1200px)");
const showLabel = showLabelProp ?? wideViewport;
const label = getLabel(status, latencyMs, retryCount);
return ( return (
<Group gap="xs"> <Group gap="xs">
<Dot <Dot
bg={statusColorVar[status]} bg={statusColorVar[status]}
className={status === "connecting" ? "inspector-pulse" : undefined} className={status === "connecting" ? "inspector-pulse" : undefined}
title={showLabel ? undefined : label}
/> />
<Text size="sm">{getLabel(status, latencyMs, retryCount)}</Text> {showLabel && <Text size="sm">{label}</Text>}
</Group> </Group>
); );
} }

View file

@ -7,7 +7,7 @@ export interface TransportBadgeProps {
const transportLabel: Record<ServerType, string> = { const transportLabel: Record<ServerType, string> = {
stdio: "STDIO", stdio: "STDIO",
sse: "SSE", sse: "HTTP",
"streamable-http": "HTTP", "streamable-http": "HTTP",
}; };

View file

@ -1,7 +1,13 @@
import type { InspectorServerJsonDraft } from "@inspector/core/mcp/types.js";
import type { Meta, StoryObj } from "@storybook/react-vite"; import type { Meta, StoryObj } from "@storybook/react-vite";
import { fn } from "storybook/test"; import { fn } from "storybook/test";
import { ImportServerJsonPanel } from "./ImportServerJsonPanel"; import { ImportServerJsonPanel } from "./ImportServerJsonPanel";
const emptyDraft: InspectorServerJsonDraft = {
rawText: "",
envOverrides: {},
};
const meta: Meta<typeof ImportServerJsonPanel> = { const meta: Meta<typeof ImportServerJsonPanel> = {
title: "Groups/ImportServerJsonPanel", title: "Groups/ImportServerJsonPanel",
component: ImportServerJsonPanel, component: ImportServerJsonPanel,
@ -13,10 +19,8 @@ const meta: Meta<typeof ImportServerJsonPanel> = {
onServerNameChange: fn(), onServerNameChange: fn(),
onAddServer: fn(), onAddServer: fn(),
onCancel: fn(), onCancel: fn(),
selectedPackageIndex: 0,
envVars: [], envVars: [],
serverName: "", validation: [],
validationResults: [],
}, },
}; };
@ -25,28 +29,31 @@ type Story = StoryObj<typeof ImportServerJsonPanel>;
export const Empty: Story = { export const Empty: Story = {
args: { args: {
jsonContent: "", draft: emptyDraft,
}, },
}; };
export const ValidJson: Story = { export const ValidJson: Story = {
args: { args: {
jsonContent: JSON.stringify( draft: {
{ rawText: JSON.stringify(
name: "my-mcp-server", {
version: "1.0.0", name: "my-mcp-server",
packages: [ version: "1.0.0",
{ packages: [
registryType: "npm", {
identifier: "my-mcp-server", registryType: "npm",
runtimeHint: "node", identifier: "my-mcp-server",
}, runtimeHint: "node",
], },
}, ],
null, },
2, null,
), 2,
validationResults: [ ),
envOverrides: {},
},
validation: [
{ type: "success", message: "Valid server.json format detected" }, { type: "success", message: "Valid server.json format detected" },
{ type: "success", message: "Package configuration is valid" }, { type: "success", message: "Package configuration is valid" },
{ type: "info", message: "1 package found" }, { type: "info", message: "1 package found" },
@ -56,31 +63,38 @@ export const ValidJson: Story = {
export const MultiplePackages: Story = { export const MultiplePackages: Story = {
args: { args: {
jsonContent: JSON.stringify( draft: {
{ rawText: JSON.stringify(
name: "multi-server", {
packages: [ name: "multi-server",
{ packages: [
registryType: "npm", {
identifier: "@scope/server", registryType: "npm",
runtimeHint: "node", identifier: "@scope/server",
}, runtimeHint: "node",
{ },
registryType: "pip", {
identifier: "server-python", registryType: "pip",
runtimeHint: "python3", identifier: "server-python",
}, runtimeHint: "python3",
], },
}, ],
null, },
2, null,
), 2,
validationResults: [ ),
envOverrides: {},
},
validation: [
{ type: "success", message: "Valid server.json format detected" }, { type: "success", message: "Valid server.json format detected" },
{ type: "info", message: "2 packages found - select one to install" }, { type: "info", message: "2 packages found - select one to install" },
], ],
packages: [ packages: [
{ registryType: "npm", identifier: "@scope/server", runtimeHint: "node" }, {
registryType: "npm",
identifier: "@scope/server",
runtimeHint: "node",
},
{ {
registryType: "pip", registryType: "pip",
identifier: "server-python", identifier: "server-python",
@ -92,25 +106,28 @@ export const MultiplePackages: Story = {
export const WithEnvVars: Story = { export const WithEnvVars: Story = {
args: { args: {
jsonContent: JSON.stringify( draft: {
{ rawText: JSON.stringify(
name: "api-server", {
packages: [ name: "api-server",
{ packages: [
registryType: "npm", {
identifier: "api-server", registryType: "npm",
runtimeHint: "node", identifier: "api-server",
}, runtimeHint: "node",
], },
envVars: [ ],
{ name: "API_KEY", required: true }, envVars: [
{ name: "API_URL", required: false }, { name: "API_KEY", required: true },
], { name: "API_URL", required: false },
}, ],
null, },
2, null,
), 2,
validationResults: [ ),
envOverrides: {},
},
validation: [
{ type: "success", message: "Valid server.json format detected" }, { type: "success", message: "Valid server.json format detected" },
{ {
type: "warning", type: "warning",
@ -136,31 +153,36 @@ export const WithEnvVars: Story = {
export const FullyConfigured: Story = { export const FullyConfigured: Story = {
args: { args: {
jsonContent: JSON.stringify( draft: {
{ rawText: JSON.stringify(
name: "full-server", {
version: "2.1.0", name: "full-server",
packages: [ version: "2.1.0",
{ packages: [
registryType: "npm", {
identifier: "@org/full-server", registryType: "npm",
runtimeHint: "node", identifier: "@org/full-server",
}, runtimeHint: "node",
{ },
registryType: "docker", {
identifier: "org/full-server:latest", registryType: "docker",
runtimeHint: "docker", identifier: "org/full-server:latest",
}, runtimeHint: "docker",
], },
envVars: [ ],
{ name: "TOKEN", required: true }, envVars: [
{ name: "DEBUG", required: false }, { name: "TOKEN", required: true },
], { name: "DEBUG", required: false },
}, ],
null, },
2, null,
), 2,
validationResults: [ ),
selectedPackageIndex: 1,
envOverrides: { TOKEN: "sk-abc123", DEBUG: "true" },
nameOverride: "My Custom Server",
},
validation: [
{ type: "success", message: "Valid server.json format detected" }, { type: "success", message: "Valid server.json format detected" },
{ type: "success", message: "All required fields present" }, { type: "success", message: "All required fields present" },
{ type: "info", message: "2 packages found" }, { type: "info", message: "2 packages found" },
@ -181,7 +203,6 @@ export const FullyConfigured: Story = {
runtimeHint: "docker", runtimeHint: "docker",
}, },
], ],
selectedPackageIndex: 1,
envVars: [ envVars: [
{ {
name: "TOKEN", name: "TOKEN",
@ -196,6 +217,5 @@ export const FullyConfigured: Story = {
value: "true", value: "true",
}, },
], ],
serverName: "My Custom Server",
}, },
}; };

View file

@ -9,6 +9,7 @@ import {
Textarea, Textarea,
Title, Title,
} from "@mantine/core"; } from "@mantine/core";
import type { InspectorServerJsonDraft } from "@inspector/core/mcp/types.js";
export interface ValidationResult { export interface ValidationResult {
type: "success" | "warning" | "info" | "error"; type: "success" | "warning" | "info" | "error";
@ -29,12 +30,10 @@ export interface EnvVarInfo {
} }
export interface ImportServerJsonPanelProps { export interface ImportServerJsonPanelProps {
jsonContent: string; draft: InspectorServerJsonDraft;
validationResults: ValidationResult[]; validation: ValidationResult[];
packages?: PackageInfo[]; packages?: PackageInfo[];
selectedPackageIndex: number;
envVars: EnvVarInfo[]; envVars: EnvVarInfo[];
serverName: string;
onJsonChange: (content: string) => void; onJsonChange: (content: string) => void;
onValidate: () => void; onValidate: () => void;
onSelectPackage: (index: number) => void; onSelectPackage: (index: number) => void;
@ -64,12 +63,10 @@ function formatPackageLabel(pkg: PackageInfo): string {
} }
export function ImportServerJsonPanel({ export function ImportServerJsonPanel({
jsonContent, draft,
validationResults, validation,
packages, packages,
selectedPackageIndex,
envVars, envVars,
serverName,
onJsonChange, onJsonChange,
onValidate, onValidate,
onSelectPackage, onSelectPackage,
@ -85,7 +82,7 @@ export function ImportServerJsonPanel({
<HintText>Paste server.json content or drag and drop a file:</HintText> <HintText>Paste server.json content or drag and drop a file:</HintText>
<Textarea <Textarea
value={jsonContent} value={draft.rawText}
onChange={(e) => onJsonChange(e.currentTarget.value)} onChange={(e) => onJsonChange(e.currentTarget.value)}
ff="monospace" ff="monospace"
autosize autosize
@ -97,7 +94,7 @@ export function ImportServerJsonPanel({
<Title order={5}>Validation Results:</Title> <Title order={5}>Validation Results:</Title>
{validationResults.map((result, index) => { {validation.map((result, index) => {
const { icon, color } = validationIcons[result.type]; const { icon, color } = validationIcons[result.type];
return ( return (
<Group key={index} gap="xs"> <Group key={index} gap="xs">
@ -112,7 +109,7 @@ export function ImportServerJsonPanel({
<Divider /> <Divider />
<Title order={5}>Package Selection:</Title> <Title order={5}>Package Selection:</Title>
<Radio.Group <Radio.Group
value={String(selectedPackageIndex)} value={String(draft.selectedPackageIndex ?? 0)}
onChange={(value) => onSelectPackage(Number(value))} onChange={(value) => onSelectPackage(Number(value))}
> >
<Stack gap="xs"> <Stack gap="xs">
@ -151,7 +148,7 @@ export function ImportServerJsonPanel({
<TextInput <TextInput
label="Server Name (optional override)" label="Server Name (optional override)"
value={serverName} value={draft.nameOverride ?? ""}
onChange={(e) => onServerNameChange(e.currentTarget.value)} onChange={(e) => onServerNameChange(e.currentTarget.value)}
/> />

View file

@ -1,22 +1,43 @@
import type {
ConnectionState,
MCPServerConfig,
} from "@inspector/core/mcp/types.js";
import type { Meta, StoryObj } from "@storybook/react-vite"; import type { Meta, StoryObj } from "@storybook/react-vite";
import { fn } from "storybook/test"; import { fn } from "storybook/test";
import { ServerCard } from "./ServerCard"; import { ServerCard } from "./ServerCard";
const stdioConfig: MCPServerConfig = {
command: "npx -y @modelcontextprotocol/server-everything",
};
const httpConfig: MCPServerConfig = {
type: "streamable-http",
url: "https://api.example.com/mcp",
};
const connected: ConnectionState = { status: "connected" };
const disconnected: ConnectionState = { status: "disconnected" };
const connecting: ConnectionState = { status: "connecting" };
const failed: ConnectionState = {
status: "error",
retryCount: 3,
error: {
message: "Connection refused",
details:
"ECONNREFUSED 127.0.0.1:3000 - The server process exited unexpectedly.",
},
};
const meta: Meta<typeof ServerCard> = { const meta: Meta<typeof ServerCard> = {
title: "Groups/ServerCard", title: "Groups/ServerCard",
component: ServerCard, component: ServerCard,
args: { args: {
onToggleConnection: fn(), onToggleConnection: fn(),
onSetActiveServer: fn(),
onServerInfo: fn(), onServerInfo: fn(),
onSettings: fn(), onSettings: fn(),
onEdit: fn(), onEdit: fn(),
onClone: fn(), onClone: fn(),
onRemove: fn(), onRemove: fn(),
onTestSampling: fn(),
onTestElicitationForm: fn(),
onTestElicitationUrl: fn(),
onConfigureRoots: fn(),
}, },
}; };
@ -25,117 +46,88 @@ type Story = StoryObj<typeof ServerCard>;
export const Connected: Story = { export const Connected: Story = {
args: { args: {
id: "7f3b2a8e-1c4d-4e5f-9a0b-3c2d1e4f5a6b",
name: "My MCP Server", name: "My MCP Server",
version: "1.2.0", config: stdioConfig,
transport: "stdio", info: { name: "My MCP Server", version: "1.2.0" },
connectionMode: "Subprocess", connection: connected,
command: "npx -y @modelcontextprotocol/server-everything",
status: "connected",
canTestClientFeatures: false,
}, },
}; };
export const Disconnected: Story = { export const Disconnected: Story = {
args: { args: {
id: "7f3b2a8e-1c4d-4e5f-9a0b-3c2d1e4f5a6b",
name: "My MCP Server", name: "My MCP Server",
version: "1.2.0", config: stdioConfig,
transport: "stdio", info: { name: "My MCP Server", version: "1.2.0" },
connectionMode: "Subprocess", connection: disconnected,
command: "npx -y @modelcontextprotocol/server-everything",
status: "disconnected",
canTestClientFeatures: false,
}, },
}; };
export const Connecting: Story = { export const Connecting: Story = {
args: { args: {
id: "7f3b2a8e-1c4d-4e5f-9a0b-3c2d1e4f5a6b",
name: "My MCP Server", name: "My MCP Server",
version: "1.2.0", config: stdioConfig,
transport: "stdio", info: { name: "My MCP Server", version: "1.2.0" },
connectionMode: "Subprocess", connection: connecting,
command: "npx -y @modelcontextprotocol/server-everything",
status: "connecting",
canTestClientFeatures: false,
}, },
}; };
export const Failed: Story = { export const Failed: Story = {
args: { args: {
id: "7f3b2a8e-1c4d-4e5f-9a0b-3c2d1e4f5a6b",
name: "My MCP Server", name: "My MCP Server",
version: "1.2.0", config: stdioConfig,
transport: "stdio", info: { name: "My MCP Server", version: "1.2.0" },
connectionMode: "Subprocess", connection: failed,
command: "npx -y @modelcontextprotocol/server-everything",
status: "error",
retryCount: 3,
error: {
message: "Connection refused",
details:
"ECONNREFUSED 127.0.0.1:3000 - The server process exited unexpectedly.",
},
canTestClientFeatures: false,
},
};
export const WithClientFeatures: Story = {
args: {
name: "My MCP Server",
version: "1.2.0",
transport: "stdio",
connectionMode: "Subprocess",
command: "npx -y @modelcontextprotocol/server-everything",
status: "connected",
canTestClientFeatures: true,
}, },
}; };
export const LongServerName: Story = { export const LongServerName: Story = {
args: { args: {
id: "a1b2c3d4-e5f6-4789-9abc-def012345678",
name: "my-organization-super-long-experimental-mcp-server-with-many-features-v2", name: "my-organization-super-long-experimental-mcp-server-with-many-features-v2",
version: "1.0.0-beta.42", config: {
transport: "stdio", command:
connectionMode: "Subprocess", "npx -y @my-organization/super-long-experimental-mcp-server-with-many-features-v2",
command: },
"npx -y @my-organization/super-long-experimental-mcp-server-with-many-features-v2", info: {
status: "connected", name: "my-organization-super-long-experimental-mcp-server-with-many-features-v2",
canTestClientFeatures: true, version: "1.0.0-beta.42",
},
connection: connected,
}, },
}; };
export const HttpDirect: Story = { export const HttpDirect: Story = {
args: { args: {
id: "9e8d7c6b-5a4f-4321-bdc6-fedcba098765",
name: "Remote API Server", name: "Remote API Server",
version: "2.0.0", config: httpConfig,
transport: "streamable-http", info: { name: "Remote API Server", version: "2.0.0" },
connectionMode: "Direct", connection: connected,
command: "https://api.example.com/mcp",
status: "connected",
canTestClientFeatures: false,
}, },
}; };
export const CompactConnected: Story = { export const CompactConnected: Story = {
args: { args: {
id: "7f3b2a8e-1c4d-4e5f-9a0b-3c2d1e4f5a6b",
name: "My MCP Server", name: "My MCP Server",
version: "1.2.0", config: stdioConfig,
transport: "stdio", info: { name: "My MCP Server", version: "1.2.0" },
connectionMode: "Subprocess", connection: connected,
command: "npx -y @modelcontextprotocol/server-everything",
status: "connected",
canTestClientFeatures: false,
compact: true, compact: true,
}, },
}; };
export const CompactDisconnected: Story = { export const CompactDisconnected: Story = {
args: { args: {
id: "7f3b2a8e-1c4d-4e5f-9a0b-3c2d1e4f5a6b",
name: "My MCP Server", name: "My MCP Server",
version: "1.2.0", config: stdioConfig,
transport: "stdio", info: { name: "My MCP Server", version: "1.2.0" },
connectionMode: "Subprocess", connection: disconnected,
command: "npx -y @modelcontextprotocol/server-everything",
status: "disconnected",
canTestClientFeatures: false,
compact: true, compact: true,
}, },
}; };

View file

@ -1,7 +1,8 @@
import { useEffect } from "react"; import { Badge, Button, Card, Group, Stack, Text } from "@mantine/core";
import { Badge, Button, Card, Group, Menu, Stack, Text } from "@mantine/core"; import type { Implementation } from "@modelcontextprotocol/sdk/types.js";
import type { import type {
ConnectionStatus, ConnectionState,
MCPServerConfig,
ServerType, ServerType,
} from "@inspector/core/mcp/types.js"; } from "@inspector/core/mcp/types.js";
import { ServerStatusIndicator } from "../../elements/ServerStatusIndicator/ServerStatusIndicator"; import { ServerStatusIndicator } from "../../elements/ServerStatusIndicator/ServerStatusIndicator";
@ -11,36 +12,33 @@ import { ContentViewer } from "../../elements/ContentViewer/ContentViewer";
import { InlineError } from "../../elements/InlineError/InlineError"; import { InlineError } from "../../elements/InlineError/InlineError";
export interface ServerCardProps { export interface ServerCardProps {
/** Stable unique identifier — the MCPConfig.mcpServers map key. */
id: string;
/** Display label shown in the card header. May or may not equal id. */
name: string; name: string;
version?: string; config: MCPServerConfig;
transport: ServerType; info?: Implementation;
connectionMode: string; connection: ConnectionState;
command: string;
status: ConnectionStatus;
retryCount?: number;
error?: { message: string; details?: string };
canTestClientFeatures: boolean;
activeServer?: string; activeServer?: string;
onSetActiveServer?: (name: string | undefined) => void; onToggleConnection: (id: string) => void;
onToggleConnection: (connect: boolean) => void; onServerInfo: (id: string) => void;
onServerInfo: () => void; onSettings: (id: string) => void;
onSettings: () => void; onEdit: (id: string) => void;
onEdit: () => void; onClone: (id: string) => void;
onClone: () => void; onRemove: (id: string) => void;
onRemove: () => void;
onTestSampling?: () => void;
onTestElicitationForm?: () => void;
onTestElicitationUrl?: () => void;
onConfigureRoots?: () => void;
compact?: boolean; compact?: boolean;
} }
const HeaderLeft = Group.withProps({ const HeaderLeft = Group.withProps({
gap: "sm", gap: "sm",
wrap: "nowrap",
miw: 0,
flex: 1,
}); });
const HeaderRight = Group.withProps({ const HeaderRight = Group.withProps({
gap: "sm", gap: "sm",
wrap: "nowrap",
}); });
const ActionsRow = Group.withProps({ const ActionsRow = Group.withProps({
@ -50,6 +48,9 @@ const ActionsRow = Group.withProps({
const ServerName = Text.withProps({ const ServerName = Text.withProps({
fw: 600, fw: 600,
size: "lg", size: "lg",
truncate: "end",
miw: 0,
flex: 1,
}); });
const ModeText = Text.withProps({ const ModeText = Text.withProps({
@ -68,48 +69,42 @@ const RemoveButton = Button.withProps({
color: "red.6", color: "red.6",
}); });
function getTransport(config: MCPServerConfig): ServerType {
return config.type ?? "stdio";
}
const TRANSPORT_DESCRIPTION: Record<ServerType, string> = {
stdio: "Standard I/O",
sse: "SSE (Server Sent Events) [deprecated]",
"streamable-http": "Streamable HTTP",
};
function getCommandOrUrl(config: MCPServerConfig): string {
if (config.type === "sse" || config.type === "streamable-http") {
return config.url;
}
return config.command;
}
export function ServerCard({ export function ServerCard({
id,
name, name,
version, config,
transport, info,
connectionMode, connection,
command,
status,
retryCount,
error,
canTestClientFeatures,
activeServer, activeServer,
onSetActiveServer,
onToggleConnection, onToggleConnection,
onServerInfo, onServerInfo,
onSettings, onSettings,
onEdit, onEdit,
onClone, onClone,
onRemove, onRemove,
onTestSampling,
onTestElicitationForm,
onTestElicitationUrl,
onConfigureRoots,
compact = false, compact = false,
}: ServerCardProps) { }: ServerCardProps) {
const isThisConnecting = activeServer === name; const isDimmed = activeServer !== undefined && activeServer !== id;
const isDimmed = activeServer !== undefined && activeServer !== name; const transport = getTransport(config);
const displayStatus = isThisConnecting ? "connecting" : status; const commandOrUrl = getCommandOrUrl(config);
const version = info?.version;
useEffect(() => {
if (isThisConnecting) {
onToggleConnection(true);
}
}, [isThisConnecting, onToggleConnection]);
function handleToggle(connect: boolean) {
if (connect && !activeServer) {
onSetActiveServer?.(name);
} else if (!connect && activeServer && activeServer === name) {
onSetActiveServer?.(undefined);
onToggleConnection(false);
}
}
return ( return (
<Card <Card
@ -119,76 +114,59 @@ export function ServerCard({
{...(isDimmed ? { "aria-disabled": true, inert: true } : {})} {...(isDimmed ? { "aria-disabled": true, inert: true } : {})}
> >
<Stack gap="sm"> <Stack gap="sm">
<Group justify="space-between" wrap="wrap"> <Group justify="space-between" wrap="nowrap">
<HeaderLeft> <HeaderLeft>
<ServerName>{name}</ServerName> <ServerName>{name}</ServerName>
{version && <Badge variant="outline">{version}</Badge>}
</HeaderLeft> </HeaderLeft>
<HeaderRight> <HeaderRight>
<ServerStatusIndicator <ServerStatusIndicator
status={displayStatus} status={connection.status}
retryCount={retryCount} retryCount={connection.retryCount}
/> />
<ConnectionToggle <ConnectionToggle
status={displayStatus} status={connection.status}
disabled={isDimmed} disabled={isDimmed}
onConnect={() => handleToggle(true)} onToggle={() => onToggleConnection(id)}
onDisconnect={() => handleToggle(false)}
/> />
</HeaderRight> </HeaderRight>
</Group> </Group>
{!compact && ( {!compact && (
<> <>
<Group justify="space-between" mih={30}> <Group gap="sm" mih={30}>
<Group gap="sm"> {version && <Badge variant="outline">{version}</Badge>}
<TransportBadge transport={transport} /> <TransportBadge transport={transport} />
<ModeText>{connectionMode}</ModeText> <ModeText>{TRANSPORT_DESCRIPTION[transport]}</ModeText>
</Group>
{canTestClientFeatures && (
<Menu>
<Menu.Target>
<SubtleButton>Test Client Features &#x25BE;</SubtleButton>
</Menu.Target>
<Menu.Dropdown>
<Menu.Item onClick={onTestSampling}>
Simulate Sampling Request
</Menu.Item>
<Menu.Item onClick={onTestElicitationForm}>
Simulate Elicitation (Form)
</Menu.Item>
<Menu.Item onClick={onTestElicitationUrl}>
Simulate Elicitation (URL)
</Menu.Item>
<Menu.Item onClick={onConfigureRoots}>
Configure Roots
</Menu.Item>
</Menu.Dropdown>
</Menu>
)}
</Group> </Group>
<ContentViewer block={{ type: "text", text: command }} copyable /> <ContentViewer
block={{ type: "text", text: commandOrUrl }}
copyable
/>
{error && ( {connection.error && (
<InlineError <InlineError
error={{ error={{
message: error.message, message: connection.error.message,
data: error.details, data: connection.error.details,
}} }}
retryCount={retryCount} retryCount={connection.retryCount}
/> />
)} )}
<Group justify="space-between"> <Group justify="space-between">
<ActionsRow> <ActionsRow>
<SubtleButton onClick={onClone}>Clone</SubtleButton> <SubtleButton onClick={() => onClone(id)}>Clone</SubtleButton>
<SubtleButton onClick={onEdit}>Edit</SubtleButton> <SubtleButton onClick={() => onEdit(id)}>Edit</SubtleButton>
<RemoveButton onClick={onRemove}>Remove</RemoveButton> <RemoveButton onClick={() => onRemove(id)}>Remove</RemoveButton>
</ActionsRow> </ActionsRow>
<ActionsRow> <ActionsRow>
<SubtleButton onClick={onServerInfo}>Server Info</SubtleButton> <SubtleButton onClick={() => onServerInfo(id)}>
<SubtleButton onClick={onSettings}>Settings</SubtleButton> Server Info
</SubtleButton>
<SubtleButton onClick={() => onSettings(id)}>
Settings
</SubtleButton>
</ActionsRow> </ActionsRow>
</Group> </Group>
</> </>

View file

@ -1,6 +1,31 @@
import type {
ClientCapabilities,
InitializeResult,
} from "@modelcontextprotocol/sdk/types.js";
import type { Meta, StoryObj } from "@storybook/react-vite"; import type { Meta, StoryObj } from "@storybook/react-vite";
import { ServerInfoContent } from "./ServerInfoContent"; import { ServerInfoContent } from "./ServerInfoContent";
const fullResult: InitializeResult = {
protocolVersion: "2025-03-26",
serverInfo: { name: "Everything Server", version: "2.1.0" },
capabilities: {
tools: { listChanged: true },
resources: { subscribe: true, listChanged: true },
prompts: { listChanged: true },
logging: {},
completions: {},
},
instructions:
"This server provides access to the project management system. Use the list_projects tool first to discover available projects before querying tasks. Rate limiting applies: max 60 requests per minute.",
};
const fullClientCaps: ClientCapabilities = {
roots: { listChanged: true },
sampling: {},
elicitation: {},
experimental: {},
};
const meta: Meta<typeof ServerInfoContent> = { const meta: Meta<typeof ServerInfoContent> = {
title: "Groups/ServerInfoContent", title: "Groups/ServerInfoContent",
component: ServerInfoContent, component: ServerInfoContent,
@ -11,80 +36,54 @@ type Story = StoryObj<typeof ServerInfoContent>;
export const FullCapabilities: Story = { export const FullCapabilities: Story = {
args: { args: {
name: "Everything Server", initializeResult: fullResult,
version: "2.1.0", clientCapabilities: fullClientCaps,
protocolVersion: "2025-03-26",
transport: "stdio", transport: "stdio",
serverCapabilities: [
{ capability: "tools", supported: true, count: 12 },
{ capability: "resources", supported: true, count: 8 },
{ capability: "prompts", supported: true, count: 5 },
{ capability: "logging", supported: true },
{ capability: "completions", supported: true },
],
clientCapabilities: [
{ capability: "roots", supported: true, count: 3 },
{ capability: "sampling", supported: true },
{ capability: "elicitation", supported: true },
{ capability: "experimental", supported: true },
],
}, },
}; };
export const MinimalCapabilities: Story = { export const MinimalCapabilities: Story = {
args: { args: {
name: "Simple Server", initializeResult: {
version: "1.0.0", protocolVersion: "2025-03-26",
protocolVersion: "2025-03-26", serverInfo: { name: "Simple Server", version: "1.0.0" },
capabilities: {
tools: { listChanged: false },
},
},
clientCapabilities: {
roots: { listChanged: true },
},
transport: "streamable-http", transport: "streamable-http",
serverCapabilities: [
{ capability: "tools", supported: true, count: 2 },
{ capability: "resources", supported: false },
{ capability: "prompts", supported: false },
],
clientCapabilities: [
{ capability: "roots", supported: true },
{ capability: "sampling", supported: false },
{ capability: "elicitation", supported: false },
],
}, },
}; };
export const WithInstructions: Story = { export const WithInstructions: Story = {
args: { args: {
name: "Guided Server", initializeResult: fullResult,
version: "1.5.0", clientCapabilities: {
protocolVersion: "2025-03-26", roots: { listChanged: true },
sampling: {},
},
transport: "stdio", transport: "stdio",
serverCapabilities: [
{ capability: "tools", supported: true, count: 4 },
{ capability: "resources", supported: true, count: 2 },
{ capability: "prompts", supported: true, count: 1 },
],
clientCapabilities: [
{ capability: "roots", supported: true },
{ capability: "sampling", supported: true },
],
instructions:
"This server provides access to the project management system. Use the list_projects tool first to discover available projects before querying tasks. Rate limiting applies: max 60 requests per minute.",
}, },
}; };
export const WithOAuth: Story = { export const WithOAuth: Story = {
args: { args: {
name: "Authenticated Server", initializeResult: {
version: "3.0.0", protocolVersion: "2025-03-26",
protocolVersion: "2025-03-26", serverInfo: { name: "Authenticated Server", version: "3.0.0" },
capabilities: {
tools: { listChanged: true },
resources: { subscribe: true },
},
},
clientCapabilities: {
roots: { listChanged: true },
},
transport: "streamable-http", transport: "streamable-http",
serverCapabilities: [ oauth: {
{ capability: "tools", supported: true, count: 6 },
{ capability: "resources", supported: true, count: 3 },
],
clientCapabilities: [
{ capability: "roots", supported: true },
{ capability: "sampling", supported: false },
],
oauthDetails: {
authUrl: "https://auth.example.com/oauth2/authorize", authUrl: "https://auth.example.com/oauth2/authorize",
scopes: ["read", "write", "admin"], scopes: ["read", "write", "admin"],
accessToken: "eyJhbGciOiJSUzI1NiIs...truncated", accessToken: "eyJhbGciOiJSUzI1NiIs...truncated",

View file

@ -7,20 +7,27 @@ import {
Text, Text,
Title, Title,
} from "@mantine/core"; } from "@mantine/core";
import type {
ClientCapabilities,
InitializeResult,
} from "@modelcontextprotocol/sdk/types.js";
import type { ServerType } from "@inspector/core/mcp/types.js";
import { import {
CapabilityItem, CapabilityItem,
type CapabilityItemProps, type CapabilityKey,
} from "../../elements/CapabilityItem/CapabilityItem"; } from "../../elements/CapabilityItem/CapabilityItem";
export interface OAuthDetails {
authUrl?: string;
scopes?: string[];
accessToken?: string;
}
export interface ServerInfoContentProps { export interface ServerInfoContentProps {
name: string; initializeResult: InitializeResult;
version: string; clientCapabilities: ClientCapabilities;
protocolVersion: string; transport: ServerType;
transport: string; oauth?: OAuthDetails;
serverCapabilities: CapabilityItemProps[];
clientCapabilities: CapabilityItemProps[];
instructions?: string;
oauthDetails?: { authUrl?: string; scopes?: string[]; accessToken?: string };
} }
const ValueText = Text.withProps({ const ValueText = Text.withProps({
@ -32,26 +39,58 @@ function formatScopes(scopes: string[]): string {
return scopes.join(", "); return scopes.join(", ");
} }
const SERVER_CAPABILITY_KEYS: CapabilityKey[] = [
"tools",
"resources",
"prompts",
"logging",
"completions",
"tasks",
"experimental",
];
const CLIENT_CAPABILITY_KEYS: CapabilityKey[] = [
"roots",
"sampling",
"elicitation",
"experimental",
];
function getCapabilityEntries(
capabilities: Record<string, unknown>,
knownKeys: CapabilityKey[],
): { capability: CapabilityKey; supported: boolean }[] {
return knownKeys.map((key) => ({
capability: key,
supported: key in capabilities && capabilities[key] != null,
}));
}
export function ServerInfoContent({ export function ServerInfoContent({
name, initializeResult,
version,
protocolVersion,
transport,
serverCapabilities,
clientCapabilities, clientCapabilities,
instructions, transport,
oauthDetails, oauth,
}: ServerInfoContentProps) { }: ServerInfoContentProps) {
const { serverInfo, protocolVersion, capabilities, instructions } =
initializeResult;
const serverCaps = getCapabilityEntries(capabilities, SERVER_CAPABILITY_KEYS);
const clientCaps = getCapabilityEntries(
clientCapabilities,
CLIENT_CAPABILITY_KEYS,
);
return ( return (
<Stack gap="md"> <Stack gap="md">
<Title order={3}>Server Information</Title> <Title order={3}>Server Information</Title>
<SimpleGrid cols={2}> <SimpleGrid cols={2}>
<Text size="sm">Name</Text> <Text size="sm">Name</Text>
<ValueText>{name}</ValueText> <ValueText>{serverInfo.name}</ValueText>
<Text size="sm">Version</Text> <Text size="sm">Version</Text>
<ValueText>{version}</ValueText> <ValueText>{serverInfo.version ?? "—"}</ValueText>
<Text size="sm">Protocol</Text> <Text size="sm">Protocol</Text>
<ValueText>{protocolVersion}</ValueText> <ValueText>{protocolVersion}</ValueText>
@ -63,23 +102,21 @@ export function ServerInfoContent({
<SimpleGrid cols={2}> <SimpleGrid cols={2}>
<Stack gap="xs"> <Stack gap="xs">
<Title order={5}>Server Capabilities</Title> <Title order={5}>Server Capabilities</Title>
{serverCapabilities.map((cap) => ( {serverCaps.map((cap) => (
<CapabilityItem <CapabilityItem
key={cap.capability} key={cap.capability}
capability={cap.capability} capability={cap.capability}
supported={cap.supported} supported={cap.supported}
count={cap.count}
/> />
))} ))}
</Stack> </Stack>
<Stack gap="xs"> <Stack gap="xs">
<Title order={5}>Client Capabilities</Title> <Title order={5}>Client Capabilities</Title>
{clientCapabilities.map((cap) => ( {clientCaps.map((cap) => (
<CapabilityItem <CapabilityItem
key={cap.capability} key={cap.capability}
capability={cap.capability} capability={cap.capability}
supported={cap.supported} supported={cap.supported}
count={cap.count}
/> />
))} ))}
</Stack> </Stack>
@ -92,26 +129,26 @@ export function ServerInfoContent({
</Stack> </Stack>
)} )}
{oauthDetails && ( {oauth && (
<Stack gap="xs"> <Stack gap="xs">
<Title order={5}>OAuth Details</Title> <Title order={5}>OAuth Details</Title>
<Stack gap="xs"> <Stack gap="xs">
{oauthDetails.authUrl && ( {oauth.authUrl && (
<SimpleGrid cols={2}> <SimpleGrid cols={2}>
<Text size="sm">Auth URL</Text> <Text size="sm">Auth URL</Text>
<Code>{oauthDetails.authUrl}</Code> <Code>{oauth.authUrl}</Code>
</SimpleGrid> </SimpleGrid>
)} )}
{oauthDetails.scopes && oauthDetails.scopes.length > 0 && ( {oauth.scopes && oauth.scopes.length > 0 && (
<SimpleGrid cols={2}> <SimpleGrid cols={2}>
<Text size="sm">Scopes</Text> <Text size="sm">Scopes</Text>
<ValueText>{formatScopes(oauthDetails.scopes)}</ValueText> <ValueText>{formatScopes(oauth.scopes)}</ValueText>
</SimpleGrid> </SimpleGrid>
)} )}
{oauthDetails.accessToken && ( {oauth.accessToken && (
<SimpleGrid cols={2}> <SimpleGrid cols={2}>
<Text size="sm">Access Token</Text> <Text size="sm">Access Token</Text>
<Code>{oauthDetails.accessToken}</Code> <Code>{oauth.accessToken}</Code>
</SimpleGrid> </SimpleGrid>
)} )}
</Stack> </Stack>

View file

@ -1,14 +1,115 @@
import type { InspectorServerSettings } from "@inspector/core/mcp/types.js";
import type { Meta, StoryObj } from "@storybook/react-vite"; import type { Meta, StoryObj } from "@storybook/react-vite";
import { fn } from "storybook/test"; import { fn } from "storybook/test";
import { ServerSettingsForm } from "./ServerSettingsForm"; import { useArgs } from "storybook/preview-api";
import {
ServerSettingsForm,
type ServerSettingsFormProps,
} from "./ServerSettingsForm";
const defaultSettings: InspectorServerSettings = {
headers: [],
metadata: [],
connectionTimeout: 30000,
requestTimeout: 60000,
};
function InteractiveRender(args: ServerSettingsFormProps) {
const [, updateArgs] = useArgs<ServerSettingsFormProps>();
return (
<ServerSettingsForm
{...args}
onExpandedSectionsChange={(sections) => {
args.onExpandedSectionsChange(sections);
updateArgs({ expandedSections: sections });
}}
onAddHeader={() => {
args.onAddHeader();
updateArgs({
settings: {
...args.settings,
headers: [...args.settings.headers, { key: "", value: "" }],
},
});
}}
onRemoveHeader={(index) => {
args.onRemoveHeader(index);
updateArgs({
settings: {
...args.settings,
headers: args.settings.headers.filter((_, i) => i !== index),
},
});
}}
onHeaderChange={(index, key, value) => {
args.onHeaderChange(index, key, value);
updateArgs({
settings: {
...args.settings,
headers: args.settings.headers.map((h, i) =>
i === index ? { key, value } : h,
),
},
});
}}
onAddMetadata={() => {
args.onAddMetadata();
updateArgs({
settings: {
...args.settings,
metadata: [...args.settings.metadata, { key: "", value: "" }],
},
});
}}
onRemoveMetadata={(index) => {
args.onRemoveMetadata(index);
updateArgs({
settings: {
...args.settings,
metadata: args.settings.metadata.filter((_, i) => i !== index),
},
});
}}
onMetadataChange={(index, key, value) => {
args.onMetadataChange(index, key, value);
updateArgs({
settings: {
...args.settings,
metadata: args.settings.metadata.map((m, i) =>
i === index ? { key, value } : m,
),
},
});
}}
onTimeoutChange={(field, value) => {
args.onTimeoutChange(field, value);
updateArgs({
settings: { ...args.settings, [field]: value },
});
}}
onOAuthChange={(oauth) => {
args.onOAuthChange(oauth);
updateArgs({
settings: {
...args.settings,
oauthClientId: oauth.clientId,
oauthClientSecret: oauth.clientSecret,
oauthScopes: oauth.scopes,
},
});
}}
/>
);
}
const meta: Meta<typeof ServerSettingsForm> = { const meta: Meta<typeof ServerSettingsForm> = {
title: "Groups/ServerSettingsForm", title: "Groups/ServerSettingsForm",
component: ServerSettingsForm, component: ServerSettingsForm,
render: InteractiveRender,
args: { args: {
expandedSections: ["connectionMode"], expandedSections: ["headers"],
onExpandedSectionsChange: fn(), onExpandedSectionsChange: fn(),
onConnectionModeChange: fn(),
onAddHeader: fn(), onAddHeader: fn(),
onRemoveHeader: fn(), onRemoveHeader: fn(),
onHeaderChange: fn(), onHeaderChange: fn(),
@ -25,55 +126,49 @@ type Story = StoryObj<typeof ServerSettingsForm>;
export const DefaultSettings: Story = { export const DefaultSettings: Story = {
args: { args: {
connectionMode: "proxy", settings: defaultSettings,
headers: [],
metadata: [],
connectionTimeout: 30000,
requestTimeout: 60000,
}, },
}; };
export const WithHeaders: Story = { export const WithHeaders: Story = {
args: { args: {
connectionMode: "proxy", settings: {
headers: [ ...defaultSettings,
{ key: "Authorization", value: "Bearer token-abc-123" }, headers: [
{ key: "X-Custom-Header", value: "custom-value" }, { key: "Authorization", value: "Bearer token-abc-123" },
], { key: "X-Custom-Header", value: "custom-value" },
metadata: [], ],
connectionTimeout: 30000, },
requestTimeout: 60000,
}, },
}; };
export const WithOAuth: Story = { export const WithOAuth: Story = {
args: { args: {
connectionMode: "direct", settings: {
headers: [], ...defaultSettings,
metadata: [], oauthClientId: "my-client-id",
connectionTimeout: 30000, oauthClientSecret: "super-secret-value",
requestTimeout: 60000, oauthScopes: "read write admin",
oauthClientId: "my-client-id", },
oauthClientSecret: "super-secret-value",
oauthScopes: "read write admin",
}, },
}; };
export const AllConfigured: Story = { export const AllConfigured: Story = {
args: { args: {
connectionMode: "proxy", settings: {
headers: [ headers: [
{ key: "Authorization", value: "Bearer token-abc-123" }, { key: "Authorization", value: "Bearer token-abc-123" },
{ key: "X-Request-Id", value: "req-456" }, { key: "X-Request-Id", value: "req-456" },
], ],
metadata: [ metadata: [
{ key: "userId", value: "user-789" }, { key: "userId", value: "user-789" },
{ key: "sessionId", value: "session-012" }, { key: "sessionId", value: "session-012" },
], ],
connectionTimeout: 15000, connectionTimeout: 15000,
requestTimeout: 45000, requestTimeout: 45000,
oauthClientId: "my-client-id", oauthClientId: "my-client-id",
oauthClientSecret: "super-secret-value", oauthClientSecret: "super-secret-value",
oauthScopes: "read write", oauthScopes: "read write",
},
}, },
}; };

View file

@ -4,51 +4,38 @@ import {
Button, Button,
Group, Group,
NumberInput, NumberInput,
Select,
Stack, Stack,
Text, Text,
TextInput, TextInput,
} from "@mantine/core"; } from "@mantine/core";
import type {
export interface KeyValuePair { InspectorServerSettings,
key: string; OAuthSettings,
value: string; } from "@inspector/core/mcp/types.js";
}
export type ServerSettingsSection = export type ServerSettingsSection =
| "connectionMode"
| "headers" | "headers"
| "metadata" | "metadata"
| "timeouts" | "timeouts"
| "oauth"; | "oauth";
export interface ServerSettingsFormProps { export interface ServerSettingsFormProps {
connectionMode: "proxy" | "direct"; settings: InspectorServerSettings;
headers: KeyValuePair[];
metadata: KeyValuePair[];
connectionTimeout: number;
requestTimeout: number;
oauthClientId?: string;
oauthClientSecret?: string;
oauthScopes?: string;
expandedSections: ServerSettingsSection[]; expandedSections: ServerSettingsSection[];
onExpandedSectionsChange: (sections: ServerSettingsSection[]) => void; onExpandedSectionsChange: (sections: ServerSettingsSection[]) => void;
onConnectionModeChange: (mode: string) => void;
onAddHeader: () => void; onAddHeader: () => void;
onRemoveHeader: (index: number) => void; onRemoveHeader: (index: number) => void;
onHeaderChange: (index: number, key: string, value: string) => void; onHeaderChange: (index: number, key: string, value: string) => void;
onAddMetadata: () => void; onAddMetadata: () => void;
onRemoveMetadata: (index: number) => void; onRemoveMetadata: (index: number) => void;
onMetadataChange: (index: number, key: string, value: string) => void; onMetadataChange: (index: number, key: string, value: string) => void;
onTimeoutChange: (field: string, value: number) => void; onTimeoutChange: (
onOAuthChange: (field: string, value: string) => void; field: "connectionTimeout" | "requestTimeout",
value: number,
) => void;
onOAuthChange: (oauth: OAuthSettings) => void;
} }
const CONNECTION_MODE_OPTIONS = [
{ value: "proxy", label: "Via Proxy" },
{ value: "direct", label: "Direct" },
];
const RemoveIcon = ActionIcon.withProps({ const RemoveIcon = ActionIcon.withProps({
color: "red", color: "red",
variant: "subtle", variant: "subtle",
@ -75,7 +62,7 @@ function KeyValueRows({
onChange, onChange,
onRemove, onRemove,
}: { }: {
items: KeyValuePair[]; items: { key: string; value: string }[];
onChange: (index: number, key: string, value: string) => void; onChange: (index: number, key: string, value: string) => void;
onRemove: (index: number) => void; onRemove: (index: number) => void;
}) { }) {
@ -105,17 +92,9 @@ function KeyValueRows({
} }
export function ServerSettingsForm({ export function ServerSettingsForm({
connectionMode, settings,
headers,
metadata,
connectionTimeout,
requestTimeout,
oauthClientId,
oauthClientSecret,
oauthScopes,
expandedSections, expandedSections,
onExpandedSectionsChange, onExpandedSectionsChange,
onConnectionModeChange,
onAddHeader, onAddHeader,
onRemoveHeader, onRemoveHeader,
onHeaderChange, onHeaderChange,
@ -125,11 +104,21 @@ export function ServerSettingsForm({
onTimeoutChange, onTimeoutChange,
onOAuthChange, onOAuthChange,
}: ServerSettingsFormProps) { }: ServerSettingsFormProps) {
const handleTimeoutChange = (field: string) => (value: number | string) => { const handleTimeoutChange =
const numValue = (field: "connectionTimeout" | "requestTimeout") =>
typeof value === "string" ? parseInt(value, 10) || 0 : value; (value: number | string) => {
onTimeoutChange(field, numValue); const numValue =
}; typeof value === "string" ? parseInt(value, 10) || 0 : value;
onTimeoutChange(field, numValue);
};
function currentOAuth(): OAuthSettings {
return {
clientId: settings.oauthClientId ?? "",
clientSecret: settings.oauthClientSecret ?? "",
scopes: settings.oauthScopes ?? "",
};
}
return ( return (
<Accordion <Accordion
@ -140,24 +129,6 @@ export function ServerSettingsForm({
} }
variant="separated" variant="separated"
> >
<Accordion.Item value="connectionMode">
<Accordion.Control>Connection Mode</Accordion.Control>
<Accordion.Panel>
<Select
data={CONNECTION_MODE_OPTIONS}
value={connectionMode}
onChange={(value) => {
if (value) onConnectionModeChange(value);
}}
description={
connectionMode === "proxy"
? "Route through inspector proxy (required for STDIO)"
: undefined
}
/>
</Accordion.Panel>
</Accordion.Item>
<Accordion.Item value="headers"> <Accordion.Item value="headers">
<Accordion.Control>Custom Headers</Accordion.Control> <Accordion.Control>Custom Headers</Accordion.Control>
<Accordion.Panel> <Accordion.Panel>
@ -168,11 +139,11 @@ export function ServerSettingsForm({
</HintText> </HintText>
<AddButton onClick={onAddHeader}>+ Add Header</AddButton> <AddButton onClick={onAddHeader}>+ Add Header</AddButton>
</Group> </Group>
{headers.length === 0 ? ( {settings.headers.length === 0 ? (
<EmptyHint>No custom headers configured</EmptyHint> <EmptyHint>No custom headers configured</EmptyHint>
) : ( ) : (
<KeyValueRows <KeyValueRows
items={headers} items={settings.headers}
onChange={onHeaderChange} onChange={onHeaderChange}
onRemove={onRemoveHeader} onRemove={onRemoveHeader}
/> />
@ -191,11 +162,11 @@ export function ServerSettingsForm({
</HintText> </HintText>
<AddButton onClick={onAddMetadata}>+ Add Metadata</AddButton> <AddButton onClick={onAddMetadata}>+ Add Metadata</AddButton>
</Group> </Group>
{metadata.length === 0 ? ( {settings.metadata.length === 0 ? (
<EmptyHint>No request metadata configured</EmptyHint> <EmptyHint>No request metadata configured</EmptyHint>
) : ( ) : (
<KeyValueRows <KeyValueRows
items={metadata} items={settings.metadata}
onChange={onMetadataChange} onChange={onMetadataChange}
onRemove={onRemoveMetadata} onRemove={onRemoveMetadata}
/> />
@ -211,13 +182,13 @@ export function ServerSettingsForm({
<NumberInput <NumberInput
label="Connection Timeout" label="Connection Timeout"
suffix=" ms" suffix=" ms"
value={connectionTimeout} value={settings.connectionTimeout}
onChange={handleTimeoutChange("connectionTimeout")} onChange={handleTimeoutChange("connectionTimeout")}
/> />
<NumberInput <NumberInput
label="Request Timeout" label="Request Timeout"
suffix=" ms" suffix=" ms"
value={requestTimeout} value={settings.requestTimeout}
onChange={handleTimeoutChange("requestTimeout")} onChange={handleTimeoutChange("requestTimeout")}
/> />
</Group> </Group>
@ -234,21 +205,34 @@ export function ServerSettingsForm({
</HintText> </HintText>
<TextInput <TextInput
label="Client ID" label="Client ID"
value={oauthClientId ?? ""} value={settings.oauthClientId ?? ""}
onChange={(e) => onOAuthChange("clientId", e.currentTarget.value)} onChange={(e) =>
onOAuthChange({
...currentOAuth(),
clientId: e.currentTarget.value,
})
}
/> />
<TextInput <TextInput
label="Client Secret" label="Client Secret"
value={oauthClientSecret ?? ""} value={settings.oauthClientSecret ?? ""}
type="password" type="password"
onChange={(e) => onChange={(e) =>
onOAuthChange("clientSecret", e.currentTarget.value) onOAuthChange({
...currentOAuth(),
clientSecret: e.currentTarget.value,
})
} }
/> />
<TextInput <TextInput
label="Scopes" label="Scopes"
value={oauthScopes ?? ""} value={settings.oauthScopes ?? ""}
onChange={(e) => onOAuthChange("scopes", e.currentTarget.value)} onChange={(e) =>
onOAuthChange({
...currentOAuth(),
scopes: e.currentTarget.value,
})
}
/> />
</Stack> </Stack>
</Accordion.Panel> </Accordion.Panel>

View file

@ -0,0 +1,72 @@
import type { InspectorServerSettings } from "@inspector/core/mcp/types.js";
import { AppShell } from "@mantine/core";
import type { Meta, StoryObj } from "@storybook/react-vite";
import { fn } from "storybook/test";
import { useArgs } from "storybook/preview-api";
import {
ServerSettingsModal,
type ServerSettingsModalProps,
} from "./ServerSettingsModal";
const initialSettings: InspectorServerSettings = {
headers: [
{ key: "Authorization", value: "Bearer token-abc-123" },
{ key: "X-Request-Id", value: "req-456" },
],
metadata: [{ key: "userId", value: "user-789" }],
connectionTimeout: 30000,
requestTimeout: 60000,
oauthClientId: "my-client-id",
oauthClientSecret: "super-secret-value",
oauthScopes: "read write",
};
function InteractiveRender(args: ServerSettingsModalProps) {
const [, updateArgs] = useArgs<ServerSettingsModalProps>();
return (
<AppShell>
<AppShell.Main>
<ServerSettingsModal
{...args}
onSettingsChange={(settings) => {
args.onSettingsChange(settings);
updateArgs({ settings });
}}
/>
</AppShell.Main>
</AppShell>
);
}
const meta: Meta<typeof ServerSettingsModal> = {
title: "Groups/ServerSettingsModal",
component: ServerSettingsModal,
parameters: { layout: "fullscreen" },
render: InteractiveRender,
args: {
opened: true,
onClose: fn(),
onSettingsChange: fn(),
},
};
export default meta;
type Story = StoryObj<typeof ServerSettingsModal>;
export const FullyConfigured: Story = {
args: {
settings: initialSettings,
},
};
export const EmptySettings: Story = {
args: {
settings: {
headers: [],
metadata: [],
connectionTimeout: 30000,
requestTimeout: 60000,
},
},
};

View file

@ -0,0 +1,137 @@
import { useState } from "react";
import { CloseButton, Group, Modal, Stack, Title } from "@mantine/core";
import type {
InspectorServerSettings,
OAuthSettings,
} from "@inspector/core/mcp/types.js";
import { ListToggle } from "../../elements/ListToggle/ListToggle";
import {
ServerSettingsForm,
type ServerSettingsSection,
} from "../ServerSettingsForm/ServerSettingsForm";
const ALL_SECTIONS: ServerSettingsSection[] = [
"headers",
"metadata",
"timeouts",
"oauth",
];
export interface ServerSettingsModalProps {
opened: boolean;
settings: InspectorServerSettings;
onClose: () => void;
onSettingsChange: (settings: InspectorServerSettings) => void;
}
export function ServerSettingsModal({
opened,
settings,
onClose,
onSettingsChange,
}: ServerSettingsModalProps) {
const [expandedSections, setExpandedSections] = useState<
ServerSettingsSection[]
>(["headers"]);
const allExpanded = expandedSections.length === ALL_SECTIONS.length;
function handleToggleAll() {
setExpandedSections(allExpanded ? [] : ALL_SECTIONS);
}
function handleAddHeader() {
onSettingsChange({
...settings,
headers: [...settings.headers, { key: "", value: "" }],
});
}
function handleRemoveHeader(index: number) {
onSettingsChange({
...settings,
headers: settings.headers.filter((_, i) => i !== index),
});
}
function handleHeaderChange(index: number, key: string, value: string) {
const headers = settings.headers.map((h, i) =>
i === index ? { key, value } : h,
);
onSettingsChange({ ...settings, headers });
}
function handleAddMetadata() {
onSettingsChange({
...settings,
metadata: [...settings.metadata, { key: "", value: "" }],
});
}
function handleRemoveMetadata(index: number) {
onSettingsChange({
...settings,
metadata: settings.metadata.filter((_, i) => i !== index),
});
}
function handleMetadataChange(index: number, key: string, value: string) {
const metadata = settings.metadata.map((m, i) =>
i === index ? { key, value } : m,
);
onSettingsChange({ ...settings, metadata });
}
function handleTimeoutChange(
field: "connectionTimeout" | "requestTimeout",
value: number,
) {
onSettingsChange({ ...settings, [field]: value });
}
function handleOAuthChange(oauth: OAuthSettings) {
onSettingsChange({
...settings,
oauthClientId: oauth.clientId,
oauthClientSecret: oauth.clientSecret,
oauthScopes: oauth.scopes,
});
}
return (
<Modal
opened={opened}
onClose={onClose}
withCloseButton={false}
size="lg"
centered
>
<Stack gap="md">
<Group justify="space-between" wrap="nowrap">
<ListToggle
compact={allExpanded}
variant="subtle"
onToggle={handleToggleAll}
/>
<Title order={4} ta="center" flex={1}>
Server Settings
</Title>
<CloseButton onClick={onClose} />
</Group>
<ServerSettingsForm
settings={settings}
expandedSections={expandedSections}
onExpandedSectionsChange={setExpandedSections}
onAddHeader={handleAddHeader}
onRemoveHeader={handleRemoveHeader}
onHeaderChange={handleHeaderChange}
onAddMetadata={handleAddMetadata}
onRemoveMetadata={handleRemoveMetadata}
onMetadataChange={handleMetadataChange}
onTimeoutChange={handleTimeoutChange}
onOAuthChange={handleOAuthChange}
/>
</Stack>
</Modal>
);
}

View file

@ -1,6 +1,6 @@
import { AppShell } from "@mantine/core";
import type { Meta, StoryObj } from "@storybook/react-vite"; import type { Meta, StoryObj } from "@storybook/react-vite";
import { fn } from "storybook/test"; import { fn } from "storybook/test";
import { AppShell } from "@mantine/core";
import { ViewHeader } from "./ViewHeader"; import { ViewHeader } from "./ViewHeader";
const meta: Meta<typeof ViewHeader> = { const meta: Meta<typeof ViewHeader> = {
@ -23,7 +23,7 @@ type Story = StoryObj<typeof ViewHeader>;
export const Connected: Story = { export const Connected: Story = {
args: { args: {
connected: true, connected: true,
serverName: "my-mcp-server", serverInfo: { name: "my-mcp-server", version: "1.2.0" },
status: "connected", status: "connected",
latencyMs: 23, latencyMs: 23,
activeTab: "Tools", activeTab: "Tools",
@ -31,8 +31,8 @@ export const Connected: Story = {
"Tools", "Tools",
"Resources", "Resources",
"Prompts", "Prompts",
"Logging",
"Tasks", "Tasks",
"Logs",
"History", "History",
], ],
onTabChange: fn(), onTabChange: fn(),

View file

@ -5,19 +5,23 @@ import {
Group, Group,
Image, Image,
SegmentedControl, SegmentedControl,
Select,
Text, Text,
Title, Title,
useComputedColorScheme,
} from "@mantine/core"; } from "@mantine/core";
import { useComputedColorScheme } from "@mantine/core"; import { useMediaQuery } from "@mantine/hooks";
import { MdLightMode, MdDarkMode } from "react-icons/md"; import type { Implementation } from "@modelcontextprotocol/sdk/types.js";
import { MdLightMode, MdDarkMode, MdLinkOff } from "react-icons/md";
import type { ConnectionStatus } from "@inspector/core/mcp/types.js";
import { ServerStatusIndicator } from "../../elements/ServerStatusIndicator/ServerStatusIndicator"; import { ServerStatusIndicator } from "../../elements/ServerStatusIndicator/ServerStatusIndicator";
import mcpLogo from "../../../theme/assets/MCP.svg"; import mcpLogo from "../../../theme/assets/MCP.svg";
import mcpLogoDark from "../../../theme/assets/MCP-dark.svg"; import mcpLogoDark from "../../../theme/assets/MCP-dark.svg";
interface ConnectedProps { interface ConnectedProps {
connected: true; connected: true;
serverName: string; serverInfo: Implementation;
status: "connected" | "connecting" | "error"; status: ConnectionStatus;
latencyMs?: number; latencyMs?: number;
activeTab: string; activeTab: string;
availableTabs: string[]; availableTabs: string[];
@ -37,13 +41,13 @@ const HeaderBar = Group.withProps({
h: "100%", h: "100%",
px: "md", px: "md",
wrap: "nowrap", wrap: "nowrap",
gap: 0, gap: "md",
}); });
const LeftSection = Group.withProps({ const LeftSection = Group.withProps({
gap: "md", gap: "md",
wrap: "nowrap", wrap: "nowrap",
w: "33.33%", flex: 1,
miw: 0, miw: 0,
}); });
@ -64,17 +68,20 @@ const ServerName = Text.withProps({
fw: 600, fw: 600,
size: "lg", size: "lg",
truncate: "end", truncate: "end",
maw: "calc(100% - 40px)", miw: 0,
flex: 1,
}); });
const CenterSection = Group.withProps({ const CenterSection = Group.withProps({
w: "33.33%", wrap: "nowrap",
justify: "center", flex: "0 0 auto",
}); });
const RightSection = Group.withProps({ const RightSection = Group.withProps({
gap: "sm", gap: "sm",
w: "33.33%", wrap: "nowrap",
flex: 1,
miw: 0,
justify: "flex-end", justify: "flex-end",
}); });
@ -84,6 +91,13 @@ const DisconnectButton = Button.withProps({
size: "sm", size: "sm",
}); });
const DisconnectIcon = ActionIcon.withProps({
variant: "subtle",
c: "red",
size: 36,
"aria-label": "Disconnect",
});
const ThemeToggle = ActionIcon.withProps({ const ThemeToggle = ActionIcon.withProps({
variant: "subtle", variant: "subtle",
size: 36, size: 36,
@ -99,6 +113,8 @@ const UnconnectedBar = Group.withProps({
export function ViewHeader(props: ViewHeaderProps) { export function ViewHeader(props: ViewHeaderProps) {
const colorScheme = useComputedColorScheme(); const colorScheme = useComputedColorScheme();
const ThemeIcon = colorScheme === "dark" ? MdLightMode : MdDarkMode; const ThemeIcon = colorScheme === "dark" ? MdLightMode : MdDarkMode;
const showSegmented = useMediaQuery("(min-width: 992px)");
const showDisconnectLabel = useMediaQuery("(min-width: 768px)");
if (!props.connected) { if (!props.connected) {
return ( return (
@ -120,16 +136,29 @@ export function ViewHeader(props: ViewHeaderProps) {
<LogoLink> <LogoLink>
<LogoImage src={colorScheme === "dark" ? mcpLogoDark : mcpLogo} /> <LogoImage src={colorScheme === "dark" ? mcpLogoDark : mcpLogo} />
</LogoLink> </LogoLink>
<ServerName>{props.serverName}</ServerName> <ServerName>{props.serverInfo.name}</ServerName>
</LeftSection> </LeftSection>
<CenterSection> <CenterSection>
<SegmentedControl {showSegmented ? (
value={props.activeTab} <SegmentedControl
onChange={props.onTabChange} value={props.activeTab}
data={props.availableTabs} onChange={props.onTabChange}
size="sm" data={props.availableTabs}
/> size="sm"
/>
) : (
<Select
value={props.activeTab}
onChange={(value) => value && props.onTabChange(value)}
data={props.availableTabs}
size="sm"
allowDeselect={false}
// Sized to comfortably fit the longest current tab label
// ("Resources"). Revisit if longer tabs are added.
w={140}
/>
)}
</CenterSection> </CenterSection>
<RightSection> <RightSection>
@ -137,9 +166,15 @@ export function ViewHeader(props: ViewHeaderProps) {
status={props.status} status={props.status}
latencyMs={props.latencyMs} latencyMs={props.latencyMs}
/> />
<DisconnectButton onClick={props.onDisconnect}> {showDisconnectLabel ? (
Disconnect <DisconnectButton onClick={props.onDisconnect}>
</DisconnectButton> Disconnect
</DisconnectButton>
) : (
<DisconnectIcon onClick={props.onDisconnect} title="Disconnect">
<MdLinkOff size={20} />
</DisconnectIcon>
)}
<ThemeToggle onClick={props.onToggleTheme}> <ThemeToggle onClick={props.onToggleTheme}>
<ThemeIcon size={20} /> <ThemeIcon size={20} />
</ThemeToggle> </ThemeToggle>

View file

@ -0,0 +1,140 @@
import type { LoggingLevel } from "@modelcontextprotocol/sdk/types.js";
import type { LogEntryData } from "../../elements/LogEntry/LogEntry";
const logMessages: {
level: LoggingLevel;
data: string;
logger?: string;
}[] = [
{ level: "info", data: "Server started on port 3000" },
{
level: "debug",
data: "Loading configuration from /etc/mcp/config.json",
logger: "config",
},
{
level: "warning",
data: "Deprecated API endpoint called: /v1/tools",
logger: "http",
},
{ level: "info", data: "Client connected: inspector-web-ui" },
{
level: "error",
data: "Failed to read resource: file not found at /data/missing.txt",
logger: "resources",
},
{
level: "info",
data: "Tool execution completed: search_files (245ms)",
logger: "tools",
},
{
level: "debug",
data: "Parsing JSON-RPC request body",
logger: "transport",
},
{
level: "info",
data: "Listing available tools for client session",
logger: "tools",
},
{
level: "warning",
data: "Slow query detected: 1200ms for resource lookup",
logger: "db",
},
{
level: "info",
data: "Resource subscription added: file:///config.json",
logger: "resources",
},
{
level: "debug",
data: "Heartbeat ping received from client",
logger: "session",
},
{
level: "error",
data: "Permission denied reading /etc/secrets.env",
logger: "resources",
},
{
level: "info",
data: "Prompt 'summarize' resolved with 2 messages",
logger: "prompts",
},
{
level: "debug",
data: "Cache hit for resource: db://users/42",
logger: "cache",
},
{
level: "info",
data: "Tool 'create_record' executed successfully in 89ms",
logger: "tools",
},
{
level: "warning",
data: "Client reconnection attempt #3",
logger: "session",
},
{
level: "info",
data: "New resource detected: file:///data/output.csv",
logger: "resources",
},
{
level: "debug",
data: "Serializing response payload (2.4KB)",
logger: "transport",
},
{
level: "error",
data: "Timeout waiting for tool response after 30s",
logger: "tools",
},
{
level: "info",
data: "Session initialized with capabilities: tools, resources, prompts",
},
{
level: "debug",
data: "Validating input schema for tool 'batch_process'",
logger: "tools",
},
{
level: "info",
data: "Resource template resolved: db://tables/users/rows/15",
logger: "resources",
},
{
level: "warning",
data: "Memory usage at 78% — consider increasing limits",
logger: "system",
},
{
level: "info",
data: "Client disconnected gracefully",
logger: "session",
},
{
level: "debug",
data: "Flushing log buffer to disk (128 entries)",
logger: "logging",
},
];
function pad(n: number): string {
return n.toString().padStart(2, "0");
}
export const mixedEntries: LogEntryData[] = Array.from(
{ length: 50 },
(_, i) => {
const src = logMessages[i % logMessages.length];
return {
receivedAt: new Date(`2026-03-17T10:00:${pad(i + 1)}Z`),
params: { level: src.level, data: src.data, logger: src.logger },
};
},
);

View file

@ -1,7 +1,7 @@
import type { Meta, StoryObj } from "@storybook/react-vite"; import type { Meta, StoryObj } from "@storybook/react-vite";
import type { LoggingLevel } from "@modelcontextprotocol/sdk/types.js";
import { fn } from "storybook/test"; import { fn } from "storybook/test";
import { LoggingScreen } from "./LoggingScreen"; import { LoggingScreen } from "./LoggingScreen";
import { mixedEntries } from "./LoggingScreen.fixtures";
import type { LogEntryData } from "../../elements/LogEntry/LogEntry"; import type { LogEntryData } from "../../elements/LogEntry/LogEntry";
const meta: Meta<typeof LoggingScreen> = { const meta: Meta<typeof LoggingScreen> = {
@ -28,141 +28,6 @@ export const Empty: Story = {
}, },
}; };
const logMessages: {
level: LoggingLevel;
data: string;
logger?: string;
}[] = [
{ level: "info", data: "Server started on port 3000" },
{
level: "debug",
data: "Loading configuration from /etc/mcp/config.json",
logger: "config",
},
{
level: "warning",
data: "Deprecated API endpoint called: /v1/tools",
logger: "http",
},
{ level: "info", data: "Client connected: inspector-web-ui" },
{
level: "error",
data: "Failed to read resource: file not found at /data/missing.txt",
logger: "resources",
},
{
level: "info",
data: "Tool execution completed: search_files (245ms)",
logger: "tools",
},
{
level: "debug",
data: "Parsing JSON-RPC request body",
logger: "transport",
},
{
level: "info",
data: "Listing available tools for client session",
logger: "tools",
},
{
level: "warning",
data: "Slow query detected: 1200ms for resource lookup",
logger: "db",
},
{
level: "info",
data: "Resource subscription added: file:///config.json",
logger: "resources",
},
{
level: "debug",
data: "Heartbeat ping received from client",
logger: "session",
},
{
level: "error",
data: "Permission denied reading /etc/secrets.env",
logger: "resources",
},
{
level: "info",
data: "Prompt 'summarize' resolved with 2 messages",
logger: "prompts",
},
{
level: "debug",
data: "Cache hit for resource: db://users/42",
logger: "cache",
},
{
level: "info",
data: "Tool 'create_record' executed successfully in 89ms",
logger: "tools",
},
{
level: "warning",
data: "Client reconnection attempt #3",
logger: "session",
},
{
level: "info",
data: "New resource detected: file:///data/output.csv",
logger: "resources",
},
{
level: "debug",
data: "Serializing response payload (2.4KB)",
logger: "transport",
},
{
level: "error",
data: "Timeout waiting for tool response after 30s",
logger: "tools",
},
{
level: "info",
data: "Session initialized with capabilities: tools, resources, prompts",
},
{
level: "debug",
data: "Validating input schema for tool 'batch_process'",
logger: "tools",
},
{
level: "info",
data: "Resource template resolved: db://tables/users/rows/15",
logger: "resources",
},
{
level: "warning",
data: "Memory usage at 78% — consider increasing limits",
logger: "system",
},
{
level: "info",
data: "Client disconnected gracefully",
logger: "session",
},
{
level: "debug",
data: "Flushing log buffer to disk (128 entries)",
logger: "logging",
},
];
function pad(n: number): string {
return n.toString().padStart(2, "0");
}
const mixedEntries: LogEntryData[] = Array.from({ length: 50 }, (_, i) => {
const src = logMessages[i % logMessages.length];
return {
receivedAt: new Date(`2026-03-17T10:00:${pad(i + 1)}Z`),
params: { level: src.level, data: src.data, logger: src.logger },
};
});
export const WithEntries: Story = { export const WithEntries: Story = {
args: { args: {
entries: mixedEntries, entries: mixedEntries,

View file

@ -1,7 +1,6 @@
import type { Meta, StoryObj } from "@storybook/react-vite"; import type { Meta, StoryObj } from "@storybook/react-vite";
import { fn } from "storybook/test"; import { fn } from "storybook/test";
import { ServerListScreen } from "./ServerListScreen"; import { ServerListScreen, type ServerEntry } from "./ServerListScreen";
import type { ServerCardProps } from "../../groups/ServerCard/ServerCard";
const meta: Meta<typeof ServerListScreen> = { const meta: Meta<typeof ServerListScreen> = {
title: "Screens/ServerListScreen", title: "Screens/ServerListScreen",
@ -11,85 +10,64 @@ const meta: Meta<typeof ServerListScreen> = {
onAddManually: fn(), onAddManually: fn(),
onImportConfig: fn(), onImportConfig: fn(),
onImportServerJson: fn(), onImportServerJson: fn(),
},
};
export default meta;
type Story = StoryObj<typeof ServerListScreen>;
function makeServerCallbacks(): Pick<
ServerCardProps,
| "onToggleConnection"
| "onServerInfo"
| "onSettings"
| "onEdit"
| "onClone"
| "onRemove"
| "onTestSampling"
| "onTestElicitationForm"
| "onTestElicitationUrl"
| "onConfigureRoots"
> {
return {
onToggleConnection: fn(), onToggleConnection: fn(),
onServerInfo: fn(), onServerInfo: fn(),
onSettings: fn(), onSettings: fn(),
onEdit: fn(), onEdit: fn(),
onClone: fn(), onClone: fn(),
onRemove: fn(), onRemove: fn(),
onTestSampling: fn(),
onTestElicitationForm: fn(),
onTestElicitationUrl: fn(),
onConfigureRoots: fn(),
};
}
const connectedStdioServer: ServerCardProps = {
name: "Local Dev Server",
version: "1.2.0",
transport: "stdio",
connectionMode: "Direct",
command: "npx @modelcontextprotocol/server-filesystem /home/user/projects",
status: "connected",
canTestClientFeatures: true,
...makeServerCallbacks(),
};
const disconnectedStdioServer: ServerCardProps = {
name: "Database Tools",
version: "0.9.1",
transport: "stdio",
connectionMode: "Direct",
command: "python -m mcp_server_sqlite --db-path ./data.db",
status: "disconnected",
canTestClientFeatures: false,
...makeServerCallbacks(),
};
const failedHttpServer: ServerCardProps = {
name: "Remote API Server",
version: "2.0.0",
transport: "streamable-http",
connectionMode: "Streamable HTTP",
command: "https://api.example.com/mcp",
status: "error",
retryCount: 3,
error: {
message: "Connection refused",
details: "ECONNREFUSED 127.0.0.1:8080 - The server may not be running.",
}, },
canTestClientFeatures: false,
...makeServerCallbacks(),
}; };
const connectingHttpServer: ServerCardProps = { export default meta;
type Story = StoryObj<typeof ServerListScreen>;
const connectedStdioServer: ServerEntry = {
id: "5e8c3d1f-2a4b-4c6d-8e7f-1a2b3c4d5e6f",
name: "Local Dev Server",
config: {
command: "npx @modelcontextprotocol/server-filesystem /home/user/projects",
},
info: { name: "Local Dev Server", version: "1.2.0" },
connection: { status: "connected" },
};
const disconnectedStdioServer: ServerEntry = {
id: "b3a7c1d2-9f8e-4a5b-bc6d-7e8f9a0b1c2d",
name: "Database Tools",
config: {
command: "python -m mcp_server_sqlite --db-path ./data.db",
},
info: { name: "Database Tools", version: "0.9.1" },
connection: { status: "disconnected" },
};
const failedHttpServer: ServerEntry = {
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: "error",
retryCount: 3,
error: {
message: "Connection refused",
details: "ECONNREFUSED 127.0.0.1:8080 - The server may not be running.",
},
},
};
const connectingHttpServer: ServerEntry = {
id: "d6e7f8a9-0b1c-4d2e-bf3a-4b5c6d7e8f90",
name: "Staging Server", name: "Staging Server",
transport: "streamable-http", config: {
connectionMode: "Streamable HTTP", type: "streamable-http",
command: "https://staging.example.com/mcp", url: "https://staging.example.com/mcp",
status: "connecting", },
canTestClientFeatures: false, connection: { status: "connecting" },
...makeServerCallbacks(),
}; };
export const MultipleServers: Story = { export const MultipleServers: Story = {
@ -120,3 +98,10 @@ export const MixedStates: Story = {
], ],
}, },
}; };
export const WithActiveServer: Story = {
args: {
servers: [connectedStdioServer, disconnectedStdioServer, failedHttpServer],
activeServer: connectedStdioServer.id,
},
};

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