Refactor ServerCard stack to use id-based callbacks
Distinguishes server `id` (the unique map key in MCPConfig.mcpServers)
from `name` (display label) so containers can dispatch by id without
relying on display strings. ServerCard, ConnectionToggle, ServerListScreen,
and supporting stories all updated.
- ServerCard: add `id` prop; all action callbacks now `(id: string) => void`
- ConnectionToggle: collapse `onConnect`/`onDisconnect` into single `onToggle`
- ServerListScreen: lift `activeServer` to a prop; combine `onSetActive` +
`onToggleConnection` into a single `onToggleConnection(id)` callback
- Stories: use UUIDs for server ids so callbacks visibly carry ids, not names
Removes the "Direct"/"Via Proxy" connection mode concept, which is no
longer planned. ServerSettingsForm drops the section, ServerCard drops
the badge, InspectorServerSettings drops the field.
Removes the "Test Client Features" dropdown from ServerCard along with
its four handlers; that surface will live elsewhere.
TransportBadge now collapses `sse` and `streamable-http` into a single
"HTTP" label; ServerCard adds a grey transport descriptor next to the
badge ("Standard I/O", "SSE (Server Sent Events) [deprecated]",
"Streamable HTTP"). The version badge moves from the header row to sit
beside the transport badge.
ServerCard header no longer wraps; long server names truncate with
ellipsis.
Specification docs (`v2_scope.md`, `v2_screenshots.md`,
`v2_ux_interfaces.md`, `v2_ux_storybook.md`) updated to match.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
29ce55290f
commit
1149288ea6
13 changed files with 196 additions and 326 deletions
|
|
@ -6,8 +6,7 @@ const meta: Meta<typeof ConnectionToggle> = {
|
|||
title: "Elements/ConnectionToggle",
|
||||
component: ConnectionToggle,
|
||||
args: {
|
||||
onConnect: fn(),
|
||||
onDisconnect: fn(),
|
||||
onToggle: fn(),
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -4,15 +4,13 @@ import type { ConnectionStatus } from "@inspector/core/mcp/types.js";
|
|||
export interface ConnectionToggleProps {
|
||||
status: ConnectionStatus;
|
||||
disabled?: boolean;
|
||||
onConnect: () => void;
|
||||
onDisconnect: () => void;
|
||||
onToggle: () => void;
|
||||
}
|
||||
|
||||
export function ConnectionToggle({
|
||||
status,
|
||||
disabled = false,
|
||||
onConnect,
|
||||
onDisconnect,
|
||||
onToggle,
|
||||
}: ConnectionToggleProps) {
|
||||
const isConnected = status === "connected";
|
||||
const isConnecting = status === "connecting";
|
||||
|
|
@ -22,13 +20,7 @@ export function ConnectionToggle({
|
|||
size="lg"
|
||||
checked={isConnected || isConnecting}
|
||||
disabled={disabled || isConnecting}
|
||||
onChange={(event) => {
|
||||
if (event.currentTarget.checked) {
|
||||
onConnect();
|
||||
} else {
|
||||
onDisconnect();
|
||||
}
|
||||
}}
|
||||
onChange={onToggle}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ export interface TransportBadgeProps {
|
|||
|
||||
const transportLabel: Record<ServerType, string> = {
|
||||
stdio: "STDIO",
|
||||
sse: "SSE",
|
||||
sse: "HTTP",
|
||||
"streamable-http": "HTTP",
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -33,16 +33,11 @@ const meta: Meta<typeof ServerCard> = {
|
|||
component: ServerCard,
|
||||
args: {
|
||||
onToggleConnection: fn(),
|
||||
onSetActiveServer: fn(),
|
||||
onServerInfo: fn(),
|
||||
onSettings: fn(),
|
||||
onEdit: fn(),
|
||||
onClone: fn(),
|
||||
onRemove: fn(),
|
||||
onTestSampling: fn(),
|
||||
onTestElicitationForm: fn(),
|
||||
onTestElicitationUrl: fn(),
|
||||
onConfigureRoots: fn(),
|
||||
},
|
||||
};
|
||||
|
||||
|
|
@ -51,61 +46,47 @@ type Story = StoryObj<typeof ServerCard>;
|
|||
|
||||
export const Connected: Story = {
|
||||
args: {
|
||||
id: "7f3b2a8e-1c4d-4e5f-9a0b-3c2d1e4f5a6b",
|
||||
name: "My MCP Server",
|
||||
config: stdioConfig,
|
||||
info: { name: "My MCP Server", version: "1.2.0" },
|
||||
connection: connected,
|
||||
connectionMode: "Subprocess",
|
||||
canTestClientFeatures: false,
|
||||
},
|
||||
};
|
||||
|
||||
export const Disconnected: Story = {
|
||||
args: {
|
||||
id: "7f3b2a8e-1c4d-4e5f-9a0b-3c2d1e4f5a6b",
|
||||
name: "My MCP Server",
|
||||
config: stdioConfig,
|
||||
info: { name: "My MCP Server", version: "1.2.0" },
|
||||
connection: disconnected,
|
||||
connectionMode: "Subprocess",
|
||||
canTestClientFeatures: false,
|
||||
},
|
||||
};
|
||||
|
||||
export const Connecting: Story = {
|
||||
args: {
|
||||
id: "7f3b2a8e-1c4d-4e5f-9a0b-3c2d1e4f5a6b",
|
||||
name: "My MCP Server",
|
||||
config: stdioConfig,
|
||||
info: { name: "My MCP Server", version: "1.2.0" },
|
||||
connection: connecting,
|
||||
connectionMode: "Subprocess",
|
||||
canTestClientFeatures: false,
|
||||
},
|
||||
};
|
||||
|
||||
export const Failed: Story = {
|
||||
args: {
|
||||
id: "7f3b2a8e-1c4d-4e5f-9a0b-3c2d1e4f5a6b",
|
||||
name: "My MCP Server",
|
||||
config: stdioConfig,
|
||||
info: { name: "My MCP Server", version: "1.2.0" },
|
||||
connection: failed,
|
||||
connectionMode: "Subprocess",
|
||||
canTestClientFeatures: false,
|
||||
},
|
||||
};
|
||||
|
||||
export const WithClientFeatures: Story = {
|
||||
args: {
|
||||
name: "My MCP Server",
|
||||
config: stdioConfig,
|
||||
info: { name: "My MCP Server", version: "1.2.0" },
|
||||
connection: connected,
|
||||
connectionMode: "Subprocess",
|
||||
canTestClientFeatures: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const LongServerName: Story = {
|
||||
args: {
|
||||
id: "a1b2c3d4-e5f6-4789-9abc-def012345678",
|
||||
name: "my-organization-super-long-experimental-mcp-server-with-many-features-v2",
|
||||
config: {
|
||||
command:
|
||||
|
|
@ -116,42 +97,37 @@ export const LongServerName: Story = {
|
|||
version: "1.0.0-beta.42",
|
||||
},
|
||||
connection: connected,
|
||||
connectionMode: "Subprocess",
|
||||
canTestClientFeatures: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const HttpDirect: Story = {
|
||||
args: {
|
||||
id: "9e8d7c6b-5a4f-4321-bdc6-fedcba098765",
|
||||
name: "Remote API Server",
|
||||
config: httpConfig,
|
||||
info: { name: "Remote API Server", version: "2.0.0" },
|
||||
connection: connected,
|
||||
connectionMode: "Direct",
|
||||
canTestClientFeatures: false,
|
||||
},
|
||||
};
|
||||
|
||||
export const CompactConnected: Story = {
|
||||
args: {
|
||||
id: "7f3b2a8e-1c4d-4e5f-9a0b-3c2d1e4f5a6b",
|
||||
name: "My MCP Server",
|
||||
config: stdioConfig,
|
||||
info: { name: "My MCP Server", version: "1.2.0" },
|
||||
connection: connected,
|
||||
connectionMode: "Subprocess",
|
||||
canTestClientFeatures: false,
|
||||
compact: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const CompactDisconnected: Story = {
|
||||
args: {
|
||||
id: "7f3b2a8e-1c4d-4e5f-9a0b-3c2d1e4f5a6b",
|
||||
name: "My MCP Server",
|
||||
config: stdioConfig,
|
||||
info: { name: "My MCP Server", version: "1.2.0" },
|
||||
connection: disconnected,
|
||||
connectionMode: "Subprocess",
|
||||
canTestClientFeatures: false,
|
||||
compact: true,
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Badge, Button, Card, Group, Menu, Stack, Text } from "@mantine/core";
|
||||
import { Badge, Button, Card, Group, Stack, Text } from "@mantine/core";
|
||||
import type { Implementation } from "@modelcontextprotocol/sdk/types.js";
|
||||
import type {
|
||||
ConnectionState,
|
||||
|
|
@ -12,33 +12,33 @@ import { ContentViewer } from "../../elements/ContentViewer/ContentViewer";
|
|||
import { InlineError } from "../../elements/InlineError/InlineError";
|
||||
|
||||
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;
|
||||
config: MCPServerConfig;
|
||||
info?: Implementation;
|
||||
connection: ConnectionState;
|
||||
connectionMode: string;
|
||||
canTestClientFeatures: boolean;
|
||||
activeServer?: string;
|
||||
onSetActiveServer?: (name: string | undefined) => void;
|
||||
onToggleConnection: (connect: boolean) => void;
|
||||
onServerInfo: () => void;
|
||||
onSettings: () => void;
|
||||
onEdit: () => void;
|
||||
onClone: () => void;
|
||||
onRemove: () => void;
|
||||
onTestSampling?: () => void;
|
||||
onTestElicitationForm?: () => void;
|
||||
onTestElicitationUrl?: () => void;
|
||||
onConfigureRoots?: () => void;
|
||||
onToggleConnection: (id: string) => void;
|
||||
onServerInfo: (id: string) => void;
|
||||
onSettings: (id: string) => void;
|
||||
onEdit: (id: string) => void;
|
||||
onClone: (id: string) => void;
|
||||
onRemove: (id: string) => void;
|
||||
compact?: boolean;
|
||||
}
|
||||
|
||||
const HeaderLeft = Group.withProps({
|
||||
gap: "sm",
|
||||
wrap: "nowrap",
|
||||
miw: 0,
|
||||
flex: 1,
|
||||
});
|
||||
|
||||
const HeaderRight = Group.withProps({
|
||||
gap: "sm",
|
||||
wrap: "nowrap",
|
||||
});
|
||||
|
||||
const ActionsRow = Group.withProps({
|
||||
|
|
@ -48,6 +48,9 @@ const ActionsRow = Group.withProps({
|
|||
const ServerName = Text.withProps({
|
||||
fw: 600,
|
||||
size: "lg",
|
||||
truncate: "end",
|
||||
miw: 0,
|
||||
flex: 1,
|
||||
});
|
||||
|
||||
const ModeText = Text.withProps({
|
||||
|
|
@ -70,6 +73,12 @@ 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;
|
||||
|
|
@ -78,27 +87,21 @@ function getCommandOrUrl(config: MCPServerConfig): string {
|
|||
}
|
||||
|
||||
export function ServerCard({
|
||||
id,
|
||||
name,
|
||||
config,
|
||||
info,
|
||||
connection,
|
||||
connectionMode,
|
||||
canTestClientFeatures,
|
||||
activeServer,
|
||||
onSetActiveServer,
|
||||
onToggleConnection,
|
||||
onServerInfo,
|
||||
onSettings,
|
||||
onEdit,
|
||||
onClone,
|
||||
onRemove,
|
||||
onTestSampling,
|
||||
onTestElicitationForm,
|
||||
onTestElicitationUrl,
|
||||
onConfigureRoots,
|
||||
compact = false,
|
||||
}: ServerCardProps) {
|
||||
const isDimmed = activeServer !== undefined && activeServer !== name;
|
||||
const isDimmed = activeServer !== undefined && activeServer !== id;
|
||||
const transport = getTransport(config);
|
||||
const commandOrUrl = getCommandOrUrl(config);
|
||||
const version = info?.version;
|
||||
|
|
@ -111,10 +114,9 @@ export function ServerCard({
|
|||
{...(isDimmed ? { "aria-disabled": true, inert: true } : {})}
|
||||
>
|
||||
<Stack gap="sm">
|
||||
<Group justify="space-between" wrap="wrap">
|
||||
<Group justify="space-between" wrap="nowrap">
|
||||
<HeaderLeft>
|
||||
<ServerName>{name}</ServerName>
|
||||
{version && <Badge variant="outline">{version}</Badge>}
|
||||
</HeaderLeft>
|
||||
<HeaderRight>
|
||||
<ServerStatusIndicator
|
||||
|
|
@ -124,46 +126,17 @@ export function ServerCard({
|
|||
<ConnectionToggle
|
||||
status={connection.status}
|
||||
disabled={isDimmed}
|
||||
onConnect={() => {
|
||||
onSetActiveServer?.(name);
|
||||
onToggleConnection(true);
|
||||
}}
|
||||
onDisconnect={() => {
|
||||
onSetActiveServer?.(undefined);
|
||||
onToggleConnection(false);
|
||||
}}
|
||||
onToggle={() => onToggleConnection(id)}
|
||||
/>
|
||||
</HeaderRight>
|
||||
</Group>
|
||||
|
||||
{!compact && (
|
||||
<>
|
||||
<Group justify="space-between" mih={30}>
|
||||
<Group gap="sm">
|
||||
<TransportBadge transport={transport} />
|
||||
<ModeText>{connectionMode}</ModeText>
|
||||
</Group>
|
||||
{canTestClientFeatures && (
|
||||
<Menu>
|
||||
<Menu.Target>
|
||||
<SubtleButton>Test Client Features ▾</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 gap="sm" mih={30}>
|
||||
{version && <Badge variant="outline">{version}</Badge>}
|
||||
<TransportBadge transport={transport} />
|
||||
<ModeText>{TRANSPORT_DESCRIPTION[transport]}</ModeText>
|
||||
</Group>
|
||||
|
||||
<ContentViewer
|
||||
|
|
@ -183,13 +156,17 @@ export function ServerCard({
|
|||
|
||||
<Group justify="space-between">
|
||||
<ActionsRow>
|
||||
<SubtleButton onClick={onClone}>Clone</SubtleButton>
|
||||
<SubtleButton onClick={onEdit}>Edit</SubtleButton>
|
||||
<RemoveButton onClick={onRemove}>Remove</RemoveButton>
|
||||
<SubtleButton onClick={() => onClone(id)}>Clone</SubtleButton>
|
||||
<SubtleButton onClick={() => onEdit(id)}>Edit</SubtleButton>
|
||||
<RemoveButton onClick={() => onRemove(id)}>Remove</RemoveButton>
|
||||
</ActionsRow>
|
||||
<ActionsRow>
|
||||
<SubtleButton onClick={onServerInfo}>Server Info</SubtleButton>
|
||||
<SubtleButton onClick={onSettings}>Settings</SubtleButton>
|
||||
<SubtleButton onClick={() => onServerInfo(id)}>
|
||||
Server Info
|
||||
</SubtleButton>
|
||||
<SubtleButton onClick={() => onSettings(id)}>
|
||||
Settings
|
||||
</SubtleButton>
|
||||
</ActionsRow>
|
||||
</Group>
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -90,7 +90,7 @@ export function ServerInfoContent({
|
|||
<ValueText>{serverInfo.name}</ValueText>
|
||||
|
||||
<Text size="sm">Version</Text>
|
||||
<ValueText>{serverInfo.version}</ValueText>
|
||||
<ValueText>{serverInfo.version ?? "—"}</ValueText>
|
||||
|
||||
<Text size="sm">Protocol</Text>
|
||||
<ValueText>{protocolVersion}</ValueText>
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ import {
|
|||
} from "./ServerSettingsForm";
|
||||
|
||||
const defaultSettings: InspectorServerSettings = {
|
||||
connectionMode: "proxy",
|
||||
headers: [],
|
||||
metadata: [],
|
||||
connectionTimeout: 30000,
|
||||
|
|
@ -25,12 +24,6 @@ function InteractiveRender(args: ServerSettingsFormProps) {
|
|||
args.onExpandedSectionsChange(sections);
|
||||
updateArgs({ expandedSections: sections });
|
||||
}}
|
||||
onConnectionModeChange={(mode) => {
|
||||
args.onConnectionModeChange(mode);
|
||||
updateArgs({
|
||||
settings: { ...args.settings, connectionMode: mode },
|
||||
});
|
||||
}}
|
||||
onAddHeader={() => {
|
||||
args.onAddHeader();
|
||||
updateArgs({
|
||||
|
|
@ -115,9 +108,8 @@ const meta: Meta<typeof ServerSettingsForm> = {
|
|||
component: ServerSettingsForm,
|
||||
render: InteractiveRender,
|
||||
args: {
|
||||
expandedSections: ["connectionMode"],
|
||||
expandedSections: ["headers"],
|
||||
onExpandedSectionsChange: fn(),
|
||||
onConnectionModeChange: fn(),
|
||||
onAddHeader: fn(),
|
||||
onRemoveHeader: fn(),
|
||||
onHeaderChange: fn(),
|
||||
|
|
@ -154,7 +146,6 @@ export const WithOAuth: Story = {
|
|||
args: {
|
||||
settings: {
|
||||
...defaultSettings,
|
||||
connectionMode: "direct",
|
||||
oauthClientId: "my-client-id",
|
||||
oauthClientSecret: "super-secret-value",
|
||||
oauthScopes: "read write admin",
|
||||
|
|
@ -165,7 +156,6 @@ export const WithOAuth: Story = {
|
|||
export const AllConfigured: Story = {
|
||||
args: {
|
||||
settings: {
|
||||
connectionMode: "proxy",
|
||||
headers: [
|
||||
{ key: "Authorization", value: "Bearer token-abc-123" },
|
||||
{ key: "X-Request-Id", value: "req-456" },
|
||||
|
|
|
|||
|
|
@ -4,15 +4,16 @@ import {
|
|||
Button,
|
||||
Group,
|
||||
NumberInput,
|
||||
Select,
|
||||
Stack,
|
||||
Text,
|
||||
TextInput,
|
||||
} from "@mantine/core";
|
||||
import type { InspectorServerSettings } from "@inspector/core/mcp/types.js";
|
||||
import type {
|
||||
InspectorServerSettings,
|
||||
OAuthSettings,
|
||||
} from "@inspector/core/mcp/types.js";
|
||||
|
||||
export type ServerSettingsSection =
|
||||
| "connectionMode"
|
||||
| "headers"
|
||||
| "metadata"
|
||||
| "timeouts"
|
||||
|
|
@ -22,28 +23,19 @@ export interface ServerSettingsFormProps {
|
|||
settings: InspectorServerSettings;
|
||||
expandedSections: ServerSettingsSection[];
|
||||
onExpandedSectionsChange: (sections: ServerSettingsSection[]) => void;
|
||||
onConnectionModeChange: (mode: "proxy" | "direct") => void;
|
||||
onAddHeader: () => void;
|
||||
onRemoveHeader: (index: number) => void;
|
||||
onHeaderChange: (index: number, key: string, value: string) => void;
|
||||
onAddMetadata: () => void;
|
||||
onRemoveMetadata: (index: number) => void;
|
||||
onMetadataChange: (index: number, key: string, value: string) => void;
|
||||
onTimeoutChange: (field: string, value: number) => void;
|
||||
onTimeoutChange: (
|
||||
field: "connectionTimeout" | "requestTimeout",
|
||||
value: number,
|
||||
) => void;
|
||||
onOAuthChange: (oauth: OAuthSettings) => void;
|
||||
}
|
||||
|
||||
export interface OAuthSettings {
|
||||
clientId: string;
|
||||
clientSecret: string;
|
||||
scopes: string;
|
||||
}
|
||||
|
||||
const CONNECTION_MODE_OPTIONS = [
|
||||
{ value: "proxy", label: "Via Proxy" },
|
||||
{ value: "direct", label: "Direct" },
|
||||
];
|
||||
|
||||
const RemoveIcon = ActionIcon.withProps({
|
||||
color: "red",
|
||||
variant: "subtle",
|
||||
|
|
@ -103,7 +95,6 @@ export function ServerSettingsForm({
|
|||
settings,
|
||||
expandedSections,
|
||||
onExpandedSectionsChange,
|
||||
onConnectionModeChange,
|
||||
onAddHeader,
|
||||
onRemoveHeader,
|
||||
onHeaderChange,
|
||||
|
|
@ -113,11 +104,13 @@ export function ServerSettingsForm({
|
|||
onTimeoutChange,
|
||||
onOAuthChange,
|
||||
}: ServerSettingsFormProps) {
|
||||
const handleTimeoutChange = (field: string) => (value: number | string) => {
|
||||
const numValue =
|
||||
typeof value === "string" ? parseInt(value, 10) || 0 : value;
|
||||
onTimeoutChange(field, numValue);
|
||||
};
|
||||
const handleTimeoutChange =
|
||||
(field: "connectionTimeout" | "requestTimeout") =>
|
||||
(value: number | string) => {
|
||||
const numValue =
|
||||
typeof value === "string" ? parseInt(value, 10) || 0 : value;
|
||||
onTimeoutChange(field, numValue);
|
||||
};
|
||||
|
||||
function currentOAuth(): OAuthSettings {
|
||||
return {
|
||||
|
|
@ -136,24 +129,6 @@ export function ServerSettingsForm({
|
|||
}
|
||||
variant="separated"
|
||||
>
|
||||
<Accordion.Item value="connectionMode">
|
||||
<Accordion.Control>Connection Mode</Accordion.Control>
|
||||
<Accordion.Panel>
|
||||
<Select
|
||||
data={CONNECTION_MODE_OPTIONS}
|
||||
value={settings.connectionMode}
|
||||
onChange={(value) => {
|
||||
if (value) onConnectionModeChange(value as "proxy" | "direct");
|
||||
}}
|
||||
description={
|
||||
settings.connectionMode === "proxy"
|
||||
? "Route through inspector proxy (required for STDIO)"
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</Accordion.Panel>
|
||||
</Accordion.Item>
|
||||
|
||||
<Accordion.Item value="headers">
|
||||
<Accordion.Control>Custom Headers</Accordion.Control>
|
||||
<Accordion.Panel>
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ import {
|
|||
} from "./ServerSettingsModal";
|
||||
|
||||
const initialSettings: InspectorServerSettings = {
|
||||
connectionMode: "proxy",
|
||||
headers: [
|
||||
{ key: "Authorization", value: "Bearer token-abc-123" },
|
||||
{ key: "X-Request-Id", value: "req-456" },
|
||||
|
|
@ -64,7 +63,6 @@ export const FullyConfigured: Story = {
|
|||
export const EmptySettings: Story = {
|
||||
args: {
|
||||
settings: {
|
||||
connectionMode: "proxy",
|
||||
headers: [],
|
||||
metadata: [],
|
||||
connectionTimeout: 30000,
|
||||
|
|
|
|||
|
|
@ -1,15 +1,16 @@
|
|||
import { useState } from "react";
|
||||
import { CloseButton, Group, Modal, Title } from "@mantine/core";
|
||||
import type { InspectorServerSettings } from "@inspector/core/mcp/types.js";
|
||||
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,
|
||||
type OAuthSettings,
|
||||
} from "../ServerSettingsForm/ServerSettingsForm";
|
||||
|
||||
const ALL_SECTIONS: ServerSettingsSection[] = [
|
||||
"connectionMode",
|
||||
"headers",
|
||||
"metadata",
|
||||
"timeouts",
|
||||
|
|
@ -31,7 +32,7 @@ export function ServerSettingsModal({
|
|||
}: ServerSettingsModalProps) {
|
||||
const [expandedSections, setExpandedSections] = useState<
|
||||
ServerSettingsSection[]
|
||||
>(["connectionMode"]);
|
||||
>(["headers"]);
|
||||
|
||||
const allExpanded = expandedSections.length === ALL_SECTIONS.length;
|
||||
|
||||
|
|
@ -39,10 +40,6 @@ export function ServerSettingsModal({
|
|||
setExpandedSections(allExpanded ? [] : ALL_SECTIONS);
|
||||
}
|
||||
|
||||
function handleConnectionModeChange(mode: "proxy" | "direct") {
|
||||
onSettingsChange({ ...settings, connectionMode: mode });
|
||||
}
|
||||
|
||||
function handleAddHeader() {
|
||||
onSettingsChange({
|
||||
...settings,
|
||||
|
|
@ -85,7 +82,10 @@ export function ServerSettingsModal({
|
|||
onSettingsChange({ ...settings, metadata });
|
||||
}
|
||||
|
||||
function handleTimeoutChange(field: string, value: number) {
|
||||
function handleTimeoutChange(
|
||||
field: "connectionTimeout" | "requestTimeout",
|
||||
value: number,
|
||||
) {
|
||||
onSettingsChange({ ...settings, [field]: value });
|
||||
}
|
||||
|
||||
|
|
@ -103,37 +103,35 @@ export function ServerSettingsModal({
|
|||
opened={opened}
|
||||
onClose={onClose}
|
||||
withCloseButton={false}
|
||||
title={
|
||||
<Group justify="space-between" wrap="nowrap" w="100%">
|
||||
size="lg"
|
||||
centered
|
||||
>
|
||||
<Stack gap="md">
|
||||
<Group justify="space-between" wrap="nowrap">
|
||||
<ListToggle
|
||||
compact={allExpanded}
|
||||
variant="subtle"
|
||||
onToggle={handleToggleAll}
|
||||
/>
|
||||
<Title order={4} ta="center" style={{ flex: 1 }}>
|
||||
<Title order={4} ta="center" flex={1}>
|
||||
Server Settings
|
||||
</Title>
|
||||
<CloseButton onClick={onClose} />
|
||||
</Group>
|
||||
}
|
||||
size="lg"
|
||||
centered
|
||||
styles={{ title: { flex: 1 } }}
|
||||
>
|
||||
<ServerSettingsForm
|
||||
settings={settings}
|
||||
expandedSections={expandedSections}
|
||||
onExpandedSectionsChange={setExpandedSections}
|
||||
onConnectionModeChange={handleConnectionModeChange}
|
||||
onAddHeader={handleAddHeader}
|
||||
onRemoveHeader={handleRemoveHeader}
|
||||
onHeaderChange={handleHeaderChange}
|
||||
onAddMetadata={handleAddMetadata}
|
||||
onRemoveMetadata={handleRemoveMetadata}
|
||||
onMetadataChange={handleMetadataChange}
|
||||
onTimeoutChange={handleTimeoutChange}
|
||||
onOAuthChange={handleOAuthChange}
|
||||
/>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { fn } from "storybook/test";
|
||||
import { ServerListScreen } from "./ServerListScreen";
|
||||
import type { ServerCardProps } from "../../groups/ServerCard/ServerCard";
|
||||
import { ServerListScreen, type ServerEntry } from "./ServerListScreen";
|
||||
|
||||
const meta: Meta<typeof ServerListScreen> = {
|
||||
title: "Screens/ServerListScreen",
|
||||
|
|
@ -11,64 +10,40 @@ const meta: Meta<typeof ServerListScreen> = {
|
|||
onAddManually: fn(),
|
||||
onImportConfig: 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(),
|
||||
onServerInfo: fn(),
|
||||
onSettings: fn(),
|
||||
onEdit: fn(),
|
||||
onClone: fn(),
|
||||
onRemove: fn(),
|
||||
onTestSampling: fn(),
|
||||
onTestElicitationForm: fn(),
|
||||
onTestElicitationUrl: fn(),
|
||||
onConfigureRoots: fn(),
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
const connectedStdioServer: 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" },
|
||||
connectionMode: "Direct",
|
||||
canTestClientFeatures: true,
|
||||
...makeServerCallbacks(),
|
||||
};
|
||||
|
||||
const disconnectedStdioServer: ServerCardProps = {
|
||||
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" },
|
||||
connectionMode: "Direct",
|
||||
canTestClientFeatures: false,
|
||||
...makeServerCallbacks(),
|
||||
};
|
||||
|
||||
const failedHttpServer: ServerCardProps = {
|
||||
const failedHttpServer: ServerEntry = {
|
||||
id: "c4d5e6f7-8a9b-4c0d-9e1f-2a3b4c5d6e7f",
|
||||
name: "Remote API Server",
|
||||
config: {
|
||||
type: "streamable-http",
|
||||
|
|
@ -83,21 +58,16 @@ const failedHttpServer: ServerCardProps = {
|
|||
details: "ECONNREFUSED 127.0.0.1:8080 - The server may not be running.",
|
||||
},
|
||||
},
|
||||
connectionMode: "Streamable HTTP",
|
||||
canTestClientFeatures: false,
|
||||
...makeServerCallbacks(),
|
||||
};
|
||||
|
||||
const connectingHttpServer: ServerCardProps = {
|
||||
const connectingHttpServer: ServerEntry = {
|
||||
id: "d6e7f8a9-0b1c-4d2e-bf3a-4b5c6d7e8f90",
|
||||
name: "Staging Server",
|
||||
config: {
|
||||
type: "streamable-http",
|
||||
url: "https://staging.example.com/mcp",
|
||||
},
|
||||
connection: { status: "connecting" },
|
||||
connectionMode: "Streamable HTTP",
|
||||
canTestClientFeatures: false,
|
||||
...makeServerCallbacks(),
|
||||
};
|
||||
|
||||
export const MultipleServers: Story = {
|
||||
|
|
|
|||
|
|
@ -4,11 +4,24 @@ import { ServerCard } from "../../groups/ServerCard/ServerCard";
|
|||
import { ServerListControls } from "../../groups/ServerListControls/ServerListControls";
|
||||
import type { ServerCardProps } from "../../groups/ServerCard/ServerCard";
|
||||
|
||||
export type ServerEntry = Pick<
|
||||
ServerCardProps,
|
||||
"id" | "name" | "config" | "info" | "connection"
|
||||
>;
|
||||
|
||||
export interface ServerListScreenProps {
|
||||
servers: ServerCardProps[];
|
||||
servers: ServerEntry[];
|
||||
/** Id of the server the wiring layer treats as active (drives card dimming). */
|
||||
activeServer?: string;
|
||||
onAddManually: () => void;
|
||||
onImportConfig: () => void;
|
||||
onImportServerJson: () => void;
|
||||
onToggleConnection: (id: string) => void;
|
||||
onServerInfo: (id: string) => void;
|
||||
onSettings: (id: string) => void;
|
||||
onEdit: (id: string) => void;
|
||||
onClone: (id: string) => void;
|
||||
onRemove: (id: string) => void;
|
||||
}
|
||||
|
||||
const PageContainer = Stack.withProps({
|
||||
|
|
@ -23,12 +36,18 @@ const EmptyState = Text.withProps({
|
|||
|
||||
export function ServerListScreen({
|
||||
servers,
|
||||
activeServer,
|
||||
onAddManually,
|
||||
onImportConfig,
|
||||
onImportServerJson,
|
||||
onToggleConnection,
|
||||
onServerInfo,
|
||||
onSettings,
|
||||
onEdit,
|
||||
onClone,
|
||||
onRemove,
|
||||
}: ServerListScreenProps) {
|
||||
const [compact, setCompact] = useState<boolean>(false);
|
||||
const [activeServer, setActiveServer] = useState<string | undefined>();
|
||||
|
||||
function handleToggleList() {
|
||||
setCompact((prev) => !prev);
|
||||
|
|
@ -58,10 +77,15 @@ export function ServerListScreen({
|
|||
>
|
||||
{servers.map((server) => (
|
||||
<ServerCard
|
||||
key={server.name}
|
||||
key={server.id}
|
||||
compact={compact}
|
||||
activeServer={activeServer}
|
||||
onSetActiveServer={setActiveServer}
|
||||
onToggleConnection={onToggleConnection}
|
||||
onServerInfo={onServerInfo}
|
||||
onSettings={onSettings}
|
||||
onEdit={onEdit}
|
||||
onClone={onClone}
|
||||
onRemove={onRemove}
|
||||
{...server}
|
||||
/>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -3,9 +3,11 @@ import type { InspectorServerSettings } from "@inspector/core/mcp/types.js";
|
|||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { fn } from "storybook/test";
|
||||
import { UnconnectedView } from "./UnconnectedView.js";
|
||||
import { ServerListScreen } from "../../screens/ServerListScreen/ServerListScreen";
|
||||
import {
|
||||
ServerListScreen,
|
||||
type ServerEntry,
|
||||
} from "../../screens/ServerListScreen/ServerListScreen";
|
||||
import { ServerSettingsModal } from "../../groups/ServerSettingsModal/ServerSettingsModal";
|
||||
import type { ServerCardProps } from "../../groups/ServerCard/ServerCard";
|
||||
|
||||
const meta: Meta<typeof UnconnectedView> = {
|
||||
title: "Views/UnconnectedView",
|
||||
|
|
@ -19,38 +21,29 @@ const meta: Meta<typeof UnconnectedView> = {
|
|||
export default meta;
|
||||
type Story = StoryObj<typeof UnconnectedView>;
|
||||
|
||||
function makeServerCallbacks(): Pick<
|
||||
ServerCardProps,
|
||||
| "onToggleConnection"
|
||||
| "onServerInfo"
|
||||
| "onSettings"
|
||||
| "onEdit"
|
||||
| "onClone"
|
||||
| "onRemove"
|
||||
> {
|
||||
return {
|
||||
onToggleConnection: fn(),
|
||||
onServerInfo: fn(),
|
||||
onSettings: fn(),
|
||||
onEdit: fn(),
|
||||
onClone: fn(),
|
||||
onRemove: fn(),
|
||||
};
|
||||
}
|
||||
const sharedScreenCallbacks = {
|
||||
onAddManually: fn(),
|
||||
onImportConfig: fn(),
|
||||
onImportServerJson: fn(),
|
||||
onToggleConnection: fn(),
|
||||
onServerInfo: fn(),
|
||||
onSettings: fn(),
|
||||
onEdit: fn(),
|
||||
onClone: fn(),
|
||||
onRemove: fn(),
|
||||
};
|
||||
|
||||
function makeStdioServer(
|
||||
name: string,
|
||||
version: string,
|
||||
command: string,
|
||||
): ServerCardProps {
|
||||
): ServerEntry {
|
||||
return {
|
||||
id: crypto.randomUUID(),
|
||||
name,
|
||||
config: { command },
|
||||
info: { name, version },
|
||||
connection: { status: "disconnected" },
|
||||
connectionMode: "Via Proxy",
|
||||
canTestClientFeatures: false,
|
||||
...makeServerCallbacks(),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -58,28 +51,19 @@ function makeHttpServer(
|
|||
name: string,
|
||||
version: string,
|
||||
url: string,
|
||||
): ServerCardProps {
|
||||
): ServerEntry {
|
||||
return {
|
||||
id: crypto.randomUUID(),
|
||||
name,
|
||||
config: { type: "streamable-http", url },
|
||||
info: { name, version },
|
||||
connection: { status: "disconnected" },
|
||||
connectionMode: "Direct",
|
||||
canTestClientFeatures: false,
|
||||
...makeServerCallbacks(),
|
||||
};
|
||||
}
|
||||
|
||||
export const Empty: Story = {
|
||||
args: {
|
||||
children: (
|
||||
<ServerListScreen
|
||||
servers={[]}
|
||||
onAddManually={fn()}
|
||||
onImportConfig={fn()}
|
||||
onImportServerJson={fn()}
|
||||
/>
|
||||
),
|
||||
children: <ServerListScreen servers={[]} {...sharedScreenCallbacks} />,
|
||||
},
|
||||
};
|
||||
|
||||
|
|
@ -104,9 +88,7 @@ export const WithServers: Story = {
|
|||
"https://api.example.com/mcp",
|
||||
),
|
||||
]}
|
||||
onAddManually={fn()}
|
||||
onImportConfig={fn()}
|
||||
onImportServerJson={fn()}
|
||||
{...sharedScreenCallbacks}
|
||||
/>
|
||||
),
|
||||
},
|
||||
|
|
@ -114,7 +96,6 @@ export const WithServers: Story = {
|
|||
|
||||
function SettingsModalStory() {
|
||||
const [settings, setSettings] = useState<InspectorServerSettings>({
|
||||
connectionMode: "proxy",
|
||||
headers: [
|
||||
{ key: "Authorization", value: "Bearer token-abc-123" },
|
||||
{ key: "X-Request-Id", value: "req-456" },
|
||||
|
|
@ -137,9 +118,7 @@ function SettingsModalStory() {
|
|||
"npx -y @modelcontextprotocol/server-everything",
|
||||
),
|
||||
]}
|
||||
onAddManually={fn()}
|
||||
onImportConfig={fn()}
|
||||
onImportServerJson={fn()}
|
||||
{...sharedScreenCallbacks}
|
||||
/>
|
||||
<ServerSettingsModal
|
||||
opened
|
||||
|
|
@ -221,9 +200,7 @@ export const ManyServers: Story = {
|
|||
"npx -y @modelcontextprotocol/server-sequential-thinking",
|
||||
),
|
||||
]}
|
||||
onAddManually={fn()}
|
||||
onImportConfig={fn()}
|
||||
onImportServerJson={fn()}
|
||||
{...sharedScreenCallbacks}
|
||||
/>
|
||||
),
|
||||
},
|
||||
|
|
|
|||
Loading…
Reference in a new issue