Phase 2: Refactor Server/Settings/ViewShell cluster to MCP SDK types

ServerCard: Replace flat scalar props (name, version, transport, status,
error) with grouped objects (config: MCPServerConfig, info?: Implementation,
connection: ConnectionState). Remove auto-connect useEffect — dumb
components must not self-dispatch side effects. Derive transport and
command/url from config.

ServerInfoContent: Replace flat props with initializeResult: InitializeResult
and clientCapabilities: ClientCapabilities from MCP SDK. Derive capability
lists by iterating the ServerCapabilities record.

ServerSettingsForm: Collapse flat setting props (connectionMode, headers,
metadata, timeouts, oauth*) into settings: InspectorServerSettings. Add
InspectorServerSettings type to core/mcp/types.ts.

ImportServerJsonPanel: Replace scattered scalar props (jsonContent,
selectedPackageIndex, serverName) with draft: InspectorServerJsonDraft.

ViewHeader: Replace serverName: string with serverInfo: Implementation.
Use ConnectionStatus from core types.

ConnectedView: Update to pass serverInfo: Implementation instead of
serverName. Use ConnectionStatus type.

Update all stories for ServerCard, ServerInfoContent, ServerSettingsForm,
ImportServerJsonPanel, ViewHeader, ServerListScreen, ConnectedView, and
UnconnectedView.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
cliffhall 2026-04-21 15:22:08 -04:00
commit 771a67ee20
16 changed files with 562 additions and 539 deletions

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,7 +1,31 @@
import type { 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 type { ConnectionState } from "./ServerCard";
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,
@ -26,11 +50,10 @@ type Story = StoryObj<typeof ServerCard>;
export const Connected: Story = { export const Connected: Story = {
args: { args: {
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" },
connection: connected,
connectionMode: "Subprocess", connectionMode: "Subprocess",
command: "npx -y @modelcontextprotocol/server-everything",
status: "connected",
canTestClientFeatures: false, canTestClientFeatures: false,
}, },
}; };
@ -38,11 +61,10 @@ export const Connected: Story = {
export const Disconnected: Story = { export const Disconnected: Story = {
args: { args: {
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" },
connection: disconnected,
connectionMode: "Subprocess", connectionMode: "Subprocess",
command: "npx -y @modelcontextprotocol/server-everything",
status: "disconnected",
canTestClientFeatures: false, canTestClientFeatures: false,
}, },
}; };
@ -50,11 +72,10 @@ export const Disconnected: Story = {
export const Connecting: Story = { export const Connecting: Story = {
args: { args: {
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" },
connection: connecting,
connectionMode: "Subprocess", connectionMode: "Subprocess",
command: "npx -y @modelcontextprotocol/server-everything",
status: "connecting",
canTestClientFeatures: false, canTestClientFeatures: false,
}, },
}; };
@ -62,17 +83,10 @@ export const Connecting: Story = {
export const Failed: Story = { export const Failed: Story = {
args: { args: {
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" },
connection: failed,
connectionMode: "Subprocess", connectionMode: "Subprocess",
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, canTestClientFeatures: false,
}, },
}; };
@ -80,11 +94,10 @@ export const Failed: Story = {
export const WithClientFeatures: Story = { export const WithClientFeatures: Story = {
args: { args: {
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" },
connection: connected,
connectionMode: "Subprocess", connectionMode: "Subprocess",
command: "npx -y @modelcontextprotocol/server-everything",
status: "connected",
canTestClientFeatures: true, canTestClientFeatures: true,
}, },
}; };
@ -92,12 +105,16 @@ export const WithClientFeatures: Story = {
export const LongServerName: Story = { export const LongServerName: Story = {
args: { args: {
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:
"npx -y @my-organization/super-long-experimental-mcp-server-with-many-features-v2",
},
info: {
name: "my-organization-super-long-experimental-mcp-server-with-many-features-v2",
version: "1.0.0-beta.42",
},
connection: connected,
connectionMode: "Subprocess", connectionMode: "Subprocess",
command:
"npx -y @my-organization/super-long-experimental-mcp-server-with-many-features-v2",
status: "connected",
canTestClientFeatures: true, canTestClientFeatures: true,
}, },
}; };
@ -105,11 +122,10 @@ export const LongServerName: Story = {
export const HttpDirect: Story = { export const HttpDirect: Story = {
args: { args: {
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" },
connection: connected,
connectionMode: "Direct", connectionMode: "Direct",
command: "https://api.example.com/mcp",
status: "connected",
canTestClientFeatures: false, canTestClientFeatures: false,
}, },
}; };
@ -117,11 +133,10 @@ export const HttpDirect: Story = {
export const CompactConnected: Story = { export const CompactConnected: Story = {
args: { args: {
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" },
connection: connected,
connectionMode: "Subprocess", connectionMode: "Subprocess",
command: "npx -y @modelcontextprotocol/server-everything",
status: "connected",
canTestClientFeatures: false, canTestClientFeatures: false,
compact: true, compact: true,
}, },
@ -130,11 +145,10 @@ export const CompactConnected: Story = {
export const CompactDisconnected: Story = { export const CompactDisconnected: Story = {
args: { args: {
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" },
connection: disconnected,
connectionMode: "Subprocess", connectionMode: "Subprocess",
command: "npx -y @modelcontextprotocol/server-everything",
status: "disconnected",
canTestClientFeatures: false, canTestClientFeatures: false,
compact: true, compact: true,
}, },

View file

@ -1,7 +1,8 @@
import { useEffect } from "react";
import { Badge, Button, Card, Group, Menu, 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, ConnectionStatus,
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";
@ -10,15 +11,18 @@ import { ConnectionToggle } from "../../elements/ConnectionToggle/ConnectionTogg
import { ContentViewer } from "../../elements/ContentViewer/ContentViewer"; import { ContentViewer } from "../../elements/ContentViewer/ContentViewer";
import { InlineError } from "../../elements/InlineError/InlineError"; import { InlineError } from "../../elements/InlineError/InlineError";
export interface ServerCardProps { export interface ConnectionState {
name: string;
version?: string;
transport: ServerType;
connectionMode: string;
command: string;
status: ConnectionStatus; status: ConnectionStatus;
retryCount?: number; retryCount?: number;
error?: { message: string; details?: string }; error?: { message: string; details?: string };
}
export interface ServerCardProps {
name: string;
config: MCPServerConfig;
info?: Implementation;
connection: ConnectionState;
connectionMode: string;
canTestClientFeatures: boolean; canTestClientFeatures: boolean;
activeServer?: string; activeServer?: string;
onSetActiveServer?: (name: string | undefined) => void; onSetActiveServer?: (name: string | undefined) => void;
@ -68,15 +72,23 @@ const RemoveButton = Button.withProps({
color: "red.6", color: "red.6",
}); });
function getTransport(config: MCPServerConfig): ServerType {
return config.type ?? "stdio";
}
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({
name, name,
version, config,
transport, info,
connection,
connectionMode, connectionMode,
command,
status,
retryCount,
error,
canTestClientFeatures, canTestClientFeatures,
activeServer, activeServer,
onSetActiveServer, onSetActiveServer,
@ -92,24 +104,10 @@ export function ServerCard({
onConfigureRoots, onConfigureRoots,
compact = false, compact = false,
}: ServerCardProps) { }: ServerCardProps) {
const isThisConnecting = activeServer === name;
const isDimmed = activeServer !== undefined && activeServer !== name; const isDimmed = activeServer !== undefined && activeServer !== name;
const displayStatus = isThisConnecting ? "connecting" : status; const transport = getTransport(config);
const commandOrUrl = getCommandOrUrl(config);
useEffect(() => { const version = info?.version;
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
@ -126,14 +124,20 @@ export function ServerCard({
</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)} onConnect={() => {
onDisconnect={() => handleToggle(false)} onSetActiveServer?.(name);
onToggleConnection(true);
}}
onDisconnect={() => {
onSetActiveServer?.(undefined);
onToggleConnection(false);
}}
/> />
</HeaderRight> </HeaderRight>
</Group> </Group>
@ -168,15 +172,18 @@ export function ServerCard({
)} )}
</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}
/> />
)} )}

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,37 @@ function formatScopes(scopes: string[]): string {
return scopes.join(", "); return scopes.join(", ");
} }
function getCapabilityEntries(
capabilities: Record<string, unknown>,
): { capability: CapabilityKey; supported: boolean }[] {
return Object.entries(capabilities).map(([key, value]) => ({
capability: key as CapabilityKey,
supported: value != 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);
const clientCaps = getCapabilityEntries(clientCapabilities);
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 +81,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 +108,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,7 +1,16 @@
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 { ServerSettingsForm } from "./ServerSettingsForm";
const defaultSettings: InspectorServerSettings = {
connectionMode: "proxy",
headers: [],
metadata: [],
connectionTimeout: 30000,
requestTimeout: 60000,
};
const meta: Meta<typeof ServerSettingsForm> = { const meta: Meta<typeof ServerSettingsForm> = {
title: "Groups/ServerSettingsForm", title: "Groups/ServerSettingsForm",
component: ServerSettingsForm, component: ServerSettingsForm,
@ -25,55 +34,51 @@ 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: [], connectionMode: "direct",
connectionTimeout: 30000, oauthClientId: "my-client-id",
requestTimeout: 60000, oauthClientSecret: "super-secret-value",
oauthClientId: "my-client-id", oauthScopes: "read write admin",
oauthClientSecret: "super-secret-value", },
oauthScopes: "read write admin",
}, },
}; };
export const AllConfigured: Story = { export const AllConfigured: Story = {
args: { args: {
connectionMode: "proxy", settings: {
headers: [ connectionMode: "proxy",
{ key: "Authorization", value: "Bearer token-abc-123" }, headers: [
{ key: "X-Request-Id", value: "req-456" }, { key: "Authorization", value: "Bearer token-abc-123" },
], { key: "X-Request-Id", value: "req-456" },
metadata: [ ],
{ key: "userId", value: "user-789" }, metadata: [
{ key: "sessionId", value: "session-012" }, { key: "userId", value: "user-789" },
], { key: "sessionId", value: "session-012" },
connectionTimeout: 15000, ],
requestTimeout: 45000, connectionTimeout: 15000,
oauthClientId: "my-client-id", requestTimeout: 45000,
oauthClientSecret: "super-secret-value", oauthClientId: "my-client-id",
oauthScopes: "read write", oauthClientSecret: "super-secret-value",
oauthScopes: "read write",
},
}, },
}; };

View file

@ -9,11 +9,7 @@ import {
Text, Text,
TextInput, TextInput,
} from "@mantine/core"; } from "@mantine/core";
import type { InspectorServerSettings } from "@inspector/core/mcp/types.js";
export interface KeyValuePair {
key: string;
value: string;
}
export type ServerSettingsSection = export type ServerSettingsSection =
| "connectionMode" | "connectionMode"
@ -23,14 +19,7 @@ export type ServerSettingsSection =
| "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; onConnectionModeChange: (mode: string) => void;
@ -75,7 +64,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,14 +94,7 @@ function KeyValueRows({
} }
export function ServerSettingsForm({ export function ServerSettingsForm({
connectionMode, settings,
headers,
metadata,
connectionTimeout,
requestTimeout,
oauthClientId,
oauthClientSecret,
oauthScopes,
expandedSections, expandedSections,
onExpandedSectionsChange, onExpandedSectionsChange,
onConnectionModeChange, onConnectionModeChange,
@ -145,12 +127,12 @@ export function ServerSettingsForm({
<Accordion.Panel> <Accordion.Panel>
<Select <Select
data={CONNECTION_MODE_OPTIONS} data={CONNECTION_MODE_OPTIONS}
value={connectionMode} value={settings.connectionMode}
onChange={(value) => { onChange={(value) => {
if (value) onConnectionModeChange(value); if (value) onConnectionModeChange(value);
}} }}
description={ description={
connectionMode === "proxy" settings.connectionMode === "proxy"
? "Route through inspector proxy (required for STDIO)" ? "Route through inspector proxy (required for STDIO)"
: undefined : undefined
} }
@ -168,11 +150,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 +173,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 +193,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,12 +216,12 @@ 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("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("clientSecret", e.currentTarget.value)
@ -247,7 +229,7 @@ export function ServerSettingsForm({
/> />
<TextInput <TextInput
label="Scopes" label="Scopes"
value={oauthScopes ?? ""} value={settings.oauthScopes ?? ""}
onChange={(e) => onOAuthChange("scopes", e.currentTarget.value)} onChange={(e) => onOAuthChange("scopes", e.currentTarget.value)}
/> />
</Stack> </Stack>

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",

View file

@ -9,15 +9,17 @@ import {
Title, Title,
} from "@mantine/core"; } from "@mantine/core";
import { useComputedColorScheme } from "@mantine/core"; import { useComputedColorScheme } from "@mantine/core";
import type { Implementation } from "@modelcontextprotocol/sdk/types.js";
import { MdLightMode, MdDarkMode } from "react-icons/md"; import { MdLightMode, MdDarkMode } 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[];
@ -120,7 +122,7 @@ 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>

View file

@ -46,48 +46,56 @@ function makeServerCallbacks(): Pick<
const connectedStdioServer: ServerCardProps = { const connectedStdioServer: ServerCardProps = {
name: "Local Dev Server", name: "Local Dev Server",
version: "1.2.0", config: {
transport: "stdio", command: "npx @modelcontextprotocol/server-filesystem /home/user/projects",
},
info: { name: "Local Dev Server", version: "1.2.0" },
connection: { status: "connected" },
connectionMode: "Direct", connectionMode: "Direct",
command: "npx @modelcontextprotocol/server-filesystem /home/user/projects",
status: "connected",
canTestClientFeatures: true, canTestClientFeatures: true,
...makeServerCallbacks(), ...makeServerCallbacks(),
}; };
const disconnectedStdioServer: ServerCardProps = { const disconnectedStdioServer: ServerCardProps = {
name: "Database Tools", name: "Database Tools",
version: "0.9.1", config: {
transport: "stdio", command: "python -m mcp_server_sqlite --db-path ./data.db",
},
info: { name: "Database Tools", version: "0.9.1" },
connection: { status: "disconnected" },
connectionMode: "Direct", connectionMode: "Direct",
command: "python -m mcp_server_sqlite --db-path ./data.db",
status: "disconnected",
canTestClientFeatures: false, canTestClientFeatures: false,
...makeServerCallbacks(), ...makeServerCallbacks(),
}; };
const failedHttpServer: ServerCardProps = { const failedHttpServer: ServerCardProps = {
name: "Remote API Server", name: "Remote API Server",
version: "2.0.0", config: {
transport: "streamable-http", type: "streamable-http",
connectionMode: "Streamable HTTP", url: "https://api.example.com/mcp",
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.",
}, },
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.",
},
},
connectionMode: "Streamable HTTP",
canTestClientFeatures: false, canTestClientFeatures: false,
...makeServerCallbacks(), ...makeServerCallbacks(),
}; };
const connectingHttpServer: ServerCardProps = { const connectingHttpServer: ServerCardProps = {
name: "Staging Server", name: "Staging Server",
transport: "streamable-http", config: {
type: "streamable-http",
url: "https://staging.example.com/mcp",
},
connection: { status: "connecting" },
connectionMode: "Streamable HTTP", connectionMode: "Streamable HTTP",
command: "https://staging.example.com/mcp",
status: "connecting",
canTestClientFeatures: false, canTestClientFeatures: false,
...makeServerCallbacks(), ...makeServerCallbacks(),
}; };

View file

@ -31,7 +31,7 @@ export function ServerListScreen({
const [activeServer, setActiveServer] = useState<string | undefined>(); const [activeServer, setActiveServer] = useState<string | undefined>();
function handleToggleList() { function handleToggleList() {
setCompact((prev) => (prev === true ? false : true)); setCompact((prev) => !prev);
} }
return ( return (

View file

@ -150,7 +150,7 @@ const meta: Meta<typeof ConnectedView> = {
component: ConnectedView, component: ConnectedView,
parameters: { layout: "fullscreen" }, parameters: { layout: "fullscreen" },
args: { args: {
serverName: "my-mcp-server", serverInfo: { name: "my-mcp-server", version: "1.0.0" },
status: "connected", status: "connected",
latencyMs: 23, latencyMs: 23,
availableTabs: allTabs, availableTabs: allTabs,
@ -705,7 +705,10 @@ export const LimitedTabs: Story = {
export const LongServerName: Story = { export const LongServerName: Story = {
args: { args: {
serverName: "my-very-long-server-name-that-might-overflow-is", serverInfo: {
name: "my-very-long-server-name-that-might-overflow-is",
version: "1.0.0",
},
children: <Text>Tools screen content</Text>, children: <Text>Tools screen content</Text>,
}, },
}; };

View file

@ -1,10 +1,12 @@
import type { ReactNode } from "react"; import type { ReactNode } from "react";
import { AppShell } from "@mantine/core"; import { AppShell } from "@mantine/core";
import type { Implementation } from "@modelcontextprotocol/sdk/types.js";
import type { ConnectionStatus } from "@inspector/core/mcp/types.js";
import { ViewHeader } from "../../groups/ViewHeader/ViewHeader"; import { ViewHeader } from "../../groups/ViewHeader/ViewHeader";
export interface ConnectedLayoutProps { export interface ConnectedLayoutProps {
serverName: string; serverInfo: Implementation;
status: "connected" | "connecting" | "error"; status: ConnectionStatus;
latencyMs?: number; latencyMs?: number;
activeTab: string; activeTab: string;
availableTabs: string[]; availableTabs: string[];
@ -15,7 +17,7 @@ export interface ConnectedLayoutProps {
} }
export function ConnectedView({ export function ConnectedView({
serverName, serverInfo,
status, status,
latencyMs, latencyMs,
activeTab, activeTab,
@ -30,7 +32,7 @@ export function ConnectedView({
<AppShell.Header> <AppShell.Header>
<ViewHeader <ViewHeader
connected connected
serverName={serverName} serverInfo={serverInfo}
status={status} status={status}
latencyMs={latencyMs} latencyMs={latencyMs}
activeTab={activeTab} activeTab={activeTab}

View file

@ -50,6 +50,38 @@ function makeServerCallbacks(): Pick<
}; };
} }
function makeStdioServer(
name: string,
version: string,
command: string,
): ServerCardProps {
return {
name,
config: { command },
info: { name, version },
connection: { status: "disconnected" },
connectionMode: "Via Proxy",
canTestClientFeatures: false,
...makeServerCallbacks(),
};
}
function makeHttpServer(
name: string,
version: string,
url: string,
): ServerCardProps {
return {
name,
config: { type: "streamable-http", url },
info: { name, version },
connection: { status: "disconnected" },
connectionMode: "Direct",
canTestClientFeatures: false,
...makeServerCallbacks(),
};
}
export const Empty: Story = { export const Empty: Story = {
args: { args: {
children: ( children: (
@ -68,37 +100,21 @@ export const WithServers: Story = {
children: ( children: (
<ServerListScreen <ServerListScreen
servers={[ servers={[
{ makeStdioServer(
name: "everything-server", "everything-server",
version: "1.0.0", "1.0.0",
transport: "stdio", "npx -y @modelcontextprotocol/server-everything",
connectionMode: "Via Proxy", ),
command: "npx -y @modelcontextprotocol/server-everything", makeStdioServer(
status: "disconnected", "filesystem-server",
canTestClientFeatures: false, "0.6.2",
...makeServerCallbacks(), "npx -y @modelcontextprotocol/server-filesystem /home/user",
}, ),
{ makeHttpServer(
name: "filesystem-server", "remote-server",
version: "0.6.2", "2.1.0",
transport: "stdio", "https://api.example.com/mcp",
connectionMode: "Via Proxy", ),
command:
"npx -y @modelcontextprotocol/server-filesystem /home/user",
status: "disconnected",
canTestClientFeatures: false,
...makeServerCallbacks(),
},
{
name: "remote-server",
version: "2.1.0",
transport: "streamable-http",
connectionMode: "Direct",
command: "https://api.example.com/mcp",
status: "disconnected",
canTestClientFeatures: false,
...makeServerCallbacks(),
},
]} ]}
onAddManually={fn()} onAddManually={fn()}
onImportConfig={fn()} onImportConfig={fn()}
@ -117,16 +133,11 @@ function SettingsModalStory() {
<UnconnectedView onToggleTheme={fn()}> <UnconnectedView onToggleTheme={fn()}>
<ServerListScreen <ServerListScreen
servers={[ servers={[
{ makeStdioServer(
name: "everything-server", "everything-server",
version: "1.0.0", "1.0.0",
transport: "stdio", "npx -y @modelcontextprotocol/server-everything",
connectionMode: "Via Proxy", ),
command: "npx -y @modelcontextprotocol/server-everything",
status: "disconnected",
canTestClientFeatures: false,
...makeServerCallbacks(),
},
]} ]}
onAddManually={fn()} onAddManually={fn()}
onImportConfig={fn()} onImportConfig={fn()}
@ -158,17 +169,19 @@ function SettingsModalStory() {
styles={{ title: { flex: 1 } }} styles={{ title: { flex: 1 } }}
> >
<ServerSettingsForm <ServerSettingsForm
connectionMode="proxy" settings={{
headers={[ connectionMode: "proxy",
{ key: "Authorization", value: "Bearer token-abc-123" }, headers: [
{ key: "X-Request-Id", value: "req-456" }, { key: "Authorization", value: "Bearer token-abc-123" },
]} { key: "X-Request-Id", value: "req-456" },
metadata={[{ key: "userId", value: "user-789" }]} ],
connectionTimeout={30000} metadata: [{ key: "userId", value: "user-789" }],
requestTimeout={60000} connectionTimeout: 30000,
oauthClientId="my-client-id" requestTimeout: 60000,
oauthClientSecret="super-secret-value" oauthClientId: "my-client-id",
oauthScopes="read write" oauthClientSecret: "super-secret-value",
oauthScopes: "read write",
}}
expandedSections={expandedSections} expandedSections={expandedSections}
onExpandedSectionsChange={setExpandedSections} onExpandedSectionsChange={setExpandedSections}
onConnectionModeChange={fn()} onConnectionModeChange={fn()}
@ -195,127 +208,66 @@ export const ManyServers: Story = {
children: ( children: (
<ServerListScreen <ServerListScreen
servers={[ servers={[
{ makeStdioServer(
name: "everything-server", "everything-server",
version: "1.0.0", "1.0.0",
transport: "stdio", "npx -y @modelcontextprotocol/server-everything",
connectionMode: "Via Proxy", ),
command: "npx -y @modelcontextprotocol/server-everything", makeStdioServer(
status: "disconnected", "filesystem-server",
canTestClientFeatures: false, "0.6.2",
...makeServerCallbacks(), "npx -y @modelcontextprotocol/server-filesystem /home/user",
}, ),
{ makeHttpServer(
name: "filesystem-server", "remote-api-server",
version: "0.6.2", "2.1.0",
transport: "stdio", "https://api.example.com/mcp",
connectionMode: "Via Proxy", ),
command: makeStdioServer(
"npx -y @modelcontextprotocol/server-filesystem /home/user", "postgres-server",
status: "disconnected", "1.3.0",
canTestClientFeatures: false, "npx -y @modelcontextprotocol/server-postgres",
...makeServerCallbacks(), ),
}, makeStdioServer(
{ "github-server",
name: "remote-api-server", "0.9.1",
version: "2.1.0", "npx -y @modelcontextprotocol/server-github",
transport: "streamable-http", ),
connectionMode: "Direct", makeHttpServer(
command: "https://api.example.com/mcp", "slack-server",
status: "disconnected", "1.1.0",
canTestClientFeatures: false, "https://slack-mcp.example.com/mcp",
...makeServerCallbacks(), ),
}, makeStdioServer(
{ "memory-server",
name: "postgres-server", "0.2.0",
version: "1.3.0", "npx -y @modelcontextprotocol/server-memory",
transport: "stdio", ),
connectionMode: "Via Proxy", makeStdioServer(
command: "npx -y @modelcontextprotocol/server-postgres", "puppeteer-server",
status: "disconnected", "1.0.3",
canTestClientFeatures: false, "npx -y @modelcontextprotocol/server-puppeteer",
...makeServerCallbacks(), ),
}, makeStdioServer(
{ "sqlite-server",
name: "github-server", "0.5.1",
version: "0.9.1", "npx -y @modelcontextprotocol/server-sqlite ./data.db",
transport: "stdio", ),
connectionMode: "Via Proxy", makeStdioServer(
command: "npx -y @modelcontextprotocol/server-github", "brave-search-server",
status: "disconnected", "1.2.0",
canTestClientFeatures: false, "npx -y @modelcontextprotocol/server-brave-search",
...makeServerCallbacks(), ),
}, makeHttpServer(
{ "google-maps-server",
name: "slack-server", "0.8.0",
version: "1.1.0", "https://maps-mcp.example.com/mcp",
transport: "streamable-http", ),
connectionMode: "Direct", makeStdioServer(
command: "https://slack-mcp.example.com/mcp", "sequential-thinking-server",
status: "disconnected", "1.4.2",
canTestClientFeatures: false, "npx -y @modelcontextprotocol/server-sequential-thinking",
...makeServerCallbacks(), ),
},
{
name: "memory-server",
version: "0.2.0",
transport: "stdio",
connectionMode: "Via Proxy",
command: "npx -y @modelcontextprotocol/server-memory",
status: "disconnected",
canTestClientFeatures: false,
...makeServerCallbacks(),
},
{
name: "puppeteer-server",
version: "1.0.3",
transport: "stdio",
connectionMode: "Via Proxy",
command: "npx -y @modelcontextprotocol/server-puppeteer",
status: "disconnected",
canTestClientFeatures: false,
...makeServerCallbacks(),
},
{
name: "sqlite-server",
version: "0.5.1",
transport: "stdio",
connectionMode: "Via Proxy",
command: "npx -y @modelcontextprotocol/server-sqlite ./data.db",
status: "disconnected",
canTestClientFeatures: false,
...makeServerCallbacks(),
},
{
name: "brave-search-server",
version: "1.2.0",
transport: "stdio",
connectionMode: "Via Proxy",
command: "npx -y @modelcontextprotocol/server-brave-search",
status: "disconnected",
canTestClientFeatures: false,
...makeServerCallbacks(),
},
{
name: "google-maps-server",
version: "0.8.0",
transport: "streamable-http",
connectionMode: "Direct",
command: "https://maps-mcp.example.com/mcp",
status: "disconnected",
canTestClientFeatures: false,
...makeServerCallbacks(),
},
{
name: "sequential-thinking-server",
version: "1.4.2",
transport: "stdio",
connectionMode: "Via Proxy",
command: "npx -y @modelcontextprotocol/server-sequential-thinking",
status: "disconnected",
canTestClientFeatures: false,
...makeServerCallbacks(),
},
]} ]}
onAddManually={fn()} onAddManually={fn()}
onImportConfig={fn()} onImportConfig={fn()}

View file

@ -173,6 +173,22 @@ export interface InspectorResourceSubscription {
lastUpdated?: Date; lastUpdated?: Date;
} }
/**
* Runtime settings for a configured server. A subset of
* InspectorClientOptions (v1.5) relevant to the settings form:
* connection mode, headers, metadata, timeouts, and OAuth credentials.
*/
export interface InspectorServerSettings {
connectionMode: "proxy" | "direct";
headers: { key: string; value: string }[];
metadata: { key: string; value: string }[];
connectionTimeout: number;
requestTimeout: number;
oauthClientId?: string;
oauthClientSecret?: string;
oauthScopes?: string;
}
/** /**
* Draft state for importing a server from registry JSON. Owned by the * Draft state for importing a server from registry JSON. Owned by the
* ImportServerJsonPanel wiring layer. `parsed` is typed `unknown` until the * ImportServerJsonPanel wiring layer. `parsed` is typed `unknown` until the