From 1149288ea6c9f40a16e76a920ff7b034725daa1a Mon Sep 17 00:00:00 2001 From: cliffhall Date: Sat, 25 Apr 2026 18:33:38 -0400 Subject: [PATCH] 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) --- .../ConnectionToggle.stories.tsx | 3 +- .../ConnectionToggle/ConnectionToggle.tsx | 14 +-- .../TransportBadge/TransportBadge.tsx | 2 +- .../groups/ServerCard/ServerCard.stories.tsx | 40 ++----- .../groups/ServerCard/ServerCard.tsx | 103 +++++++----------- .../ServerInfoContent/ServerInfoContent.tsx | 2 +- .../ServerSettingsForm.stories.tsx | 12 +- .../ServerSettingsForm/ServerSettingsForm.tsx | 55 +++------- .../ServerSettingsModal.stories.tsx | 2 - .../ServerSettingsModal.tsx | 62 +++++------ .../ServerListScreen.stories.tsx | 58 +++------- .../ServerListScreen/ServerListScreen.tsx | 32 +++++- .../UnconnectedView.stories.tsx | 69 ++++-------- core/mcp/types.ts | 14 ++- specification/v2_scope.md | 4 - specification/v2_screenshots.md | 5 - specification/v2_ux_interfaces.md | 32 +++--- specification/v2_ux_storybook.md | 13 +-- 18 files changed, 196 insertions(+), 326 deletions(-) diff --git a/clients/web/src/components/elements/ConnectionToggle/ConnectionToggle.stories.tsx b/clients/web/src/components/elements/ConnectionToggle/ConnectionToggle.stories.tsx index faa765db..2ed0b159 100644 --- a/clients/web/src/components/elements/ConnectionToggle/ConnectionToggle.stories.tsx +++ b/clients/web/src/components/elements/ConnectionToggle/ConnectionToggle.stories.tsx @@ -6,8 +6,7 @@ const meta: Meta = { title: "Elements/ConnectionToggle", component: ConnectionToggle, args: { - onConnect: fn(), - onDisconnect: fn(), + onToggle: fn(), }, }; diff --git a/clients/web/src/components/elements/ConnectionToggle/ConnectionToggle.tsx b/clients/web/src/components/elements/ConnectionToggle/ConnectionToggle.tsx index 5dcb6265..9ab70e2b 100644 --- a/clients/web/src/components/elements/ConnectionToggle/ConnectionToggle.tsx +++ b/clients/web/src/components/elements/ConnectionToggle/ConnectionToggle.tsx @@ -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} /> ); } diff --git a/clients/web/src/components/elements/TransportBadge/TransportBadge.tsx b/clients/web/src/components/elements/TransportBadge/TransportBadge.tsx index 129d88e1..2a553967 100644 --- a/clients/web/src/components/elements/TransportBadge/TransportBadge.tsx +++ b/clients/web/src/components/elements/TransportBadge/TransportBadge.tsx @@ -7,7 +7,7 @@ export interface TransportBadgeProps { const transportLabel: Record = { stdio: "STDIO", - sse: "SSE", + sse: "HTTP", "streamable-http": "HTTP", }; diff --git a/clients/web/src/components/groups/ServerCard/ServerCard.stories.tsx b/clients/web/src/components/groups/ServerCard/ServerCard.stories.tsx index 65aed920..64ff1273 100644 --- a/clients/web/src/components/groups/ServerCard/ServerCard.stories.tsx +++ b/clients/web/src/components/groups/ServerCard/ServerCard.stories.tsx @@ -33,16 +33,11 @@ const meta: Meta = { 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; 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, }, }; diff --git a/clients/web/src/components/groups/ServerCard/ServerCard.tsx b/clients/web/src/components/groups/ServerCard/ServerCard.tsx index 75f8292d..14804548 100644 --- a/clients/web/src/components/groups/ServerCard/ServerCard.tsx +++ b/clients/web/src/components/groups/ServerCard/ServerCard.tsx @@ -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 = { + 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 } : {})} > - + {name} - {version && {version}} { - onSetActiveServer?.(name); - onToggleConnection(true); - }} - onDisconnect={() => { - onSetActiveServer?.(undefined); - onToggleConnection(false); - }} + onToggle={() => onToggleConnection(id)} /> {!compact && ( <> - - - - {connectionMode} - - {canTestClientFeatures && ( - - - Test Client Features ▾ - - - - Simulate Sampling Request - - - Simulate Elicitation (Form) - - - Simulate Elicitation (URL) - - - Configure Roots - - - - )} + + {version && {version}} + + {TRANSPORT_DESCRIPTION[transport]} - Clone - Edit - Remove + onClone(id)}>Clone + onEdit(id)}>Edit + onRemove(id)}>Remove - Server Info - Settings + onServerInfo(id)}> + Server Info + + onSettings(id)}> + Settings + diff --git a/clients/web/src/components/groups/ServerInfoContent/ServerInfoContent.tsx b/clients/web/src/components/groups/ServerInfoContent/ServerInfoContent.tsx index 147d246d..407e6a78 100644 --- a/clients/web/src/components/groups/ServerInfoContent/ServerInfoContent.tsx +++ b/clients/web/src/components/groups/ServerInfoContent/ServerInfoContent.tsx @@ -90,7 +90,7 @@ export function ServerInfoContent({ {serverInfo.name} Version - {serverInfo.version} + {serverInfo.version ?? "—"} Protocol {protocolVersion} diff --git a/clients/web/src/components/groups/ServerSettingsForm/ServerSettingsForm.stories.tsx b/clients/web/src/components/groups/ServerSettingsForm/ServerSettingsForm.stories.tsx index de732b81..3d82268d 100644 --- a/clients/web/src/components/groups/ServerSettingsForm/ServerSettingsForm.stories.tsx +++ b/clients/web/src/components/groups/ServerSettingsForm/ServerSettingsForm.stories.tsx @@ -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 = { 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" }, diff --git a/clients/web/src/components/groups/ServerSettingsForm/ServerSettingsForm.tsx b/clients/web/src/components/groups/ServerSettingsForm/ServerSettingsForm.tsx index 3db4cf7f..cf9e451d 100644 --- a/clients/web/src/components/groups/ServerSettingsForm/ServerSettingsForm.tsx +++ b/clients/web/src/components/groups/ServerSettingsForm/ServerSettingsForm.tsx @@ -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" > - - Connection Mode - -