Add new components, typography, badge contrast, and screen layout refactors

New element components:
- ListToggle: expand/collapse button using react-icons (RiExpandVerticalLine/RiCollapseVerticalLine)
- LogLevelBadge: extracted from LogEntry, handles all 8 log levels with filled variant and dark/light text contrast

New group components:
- ViewHeader: shared header bar for ConnectedView and UnconnectedView with discriminated union on `connected` prop; MCP logo links to modelcontextprotocol.io
- ServerListControls: combines ListToggle and AddServerMenu, hides toggle when no servers
- LogStreamPanel: extracted from LoggingScreen with scroll containment, filtering by visible levels and search text
- HistoryControls: vertical sidebar with search, method filter, export, and clear
- HistoryListPanel: extracted from HistoryScreen with ListToggle compact mode, filtering by method and search text

Badge contrast improvements:
- AnnotationBadge: filled variant for both dark/light modes; black text in dark, white text in light
- LogLevelBadge: same contrast approach; bold weight for alert/emergency levels
- Badge theme default weight reduced from 700 to 600

Typography:
- Added Fredoka (300-700) as body and heading font
- Added Roboto Mono as monospace font
- Google Fonts loaded in index.html and Storybook previewHead
- Heading weight set to 600

Screen layout refactors:
- LoggingScreen: owns filterText and visibleLevels state; passes to LogControls and LogStreamPanel; removed controls prop in favor of flat props
- HistoryScreen: owns searchText and methodFilter state; passes to HistoryControls and HistoryListPanel for filtering
- Both screens use new ScreenLayout (Flex variant="screen") and Sidebar subcomponents
- Added Flex theme with "screen" variant (overflow: hidden)
- Added Paper theme "panel" variant (flex column, overflow hidden)

Component enhancements:
- ServerCard: added compact prop showing only header row
- HistoryEntry: renamed isExpanded to isListExpanded; internal useState syncs via useEffect; individual entries toggle independently
- LogControls: Select All / Deselect All toggle for level checkboxes; nowrap on level selector group
- ConnectionToggle: LoadingOverlay with rounded corners for connecting state
- ConnectedView: ThemeToggle ActionIcon matched to DisconnectButton height (36px) and border color (outline variant)
- Storybook preview: fullscreen layout wrapper with 100vh overflow hidden

New stories:
- UnconnectedView: Connecting and ManyServers (12 servers for scroll testing)
- ServerCard: CompactConnected and CompactDisconnected
- All new components have corresponding stories

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
cliffhall 2026-03-28 20:38:04 -04:00
commit 961dc283fc
7 changed files with 1602 additions and 790 deletions

View file

@ -1,97 +0,0 @@
import type { ReactNode } from "react";
import {
ActionIcon,
AppShell,
Button,
Group,
Image,
SegmentedControl,
Text,
} from "@mantine/core";
import { useComputedColorScheme } from "@mantine/core";
import { StatusIndicator } from "../../elements/StatusIndicator/StatusIndicator";
import mcpLogo from "../../../theme/assets/MCP.svg";
import mcpLogoDark from "../../../theme/assets/MCP-dark.svg";
export interface ConnectedLayoutProps {
serverName: string;
status: "connected" | "connecting" | "failed";
latencyMs?: number;
activeTab: string;
availableTabs: string[];
onTabChange: (tab: string) => void;
onDisconnect: () => void;
onToggleTheme: () => void;
children: ReactNode;
}
const ServerName = Text.withProps({
fw: 600,
size: "lg",
truncate: "end",
maw: "calc(100% - 40px)",
});
export function ConnectedLayout({
serverName,
status,
latencyMs,
activeTab,
availableTabs,
onTabChange,
onDisconnect,
onToggleTheme,
children,
}: ConnectedLayoutProps) {
const colorScheme = useComputedColorScheme();
return (
<AppShell header={{ height: 60 }} padding="md">
<AppShell.Header>
<Group h="100%" px="md" wrap="nowrap" gap={0}>
<Group gap="md" wrap="nowrap" w="33.33%" miw={0}>
<Image
src={colorScheme === "dark" ? mcpLogoDark : mcpLogo}
alt="MCP"
w={28}
h={28}
fit="contain"
/>
<ServerName>{serverName}</ServerName>
</Group>
<Group w="33.33%" justify="center">
<SegmentedControl
value={activeTab}
onChange={onTabChange}
data={availableTabs}
size="sm"
/>
</Group>
<Group gap="sm" w="33.33%" justify="flex-end">
<StatusIndicator status={status} latencyMs={latencyMs} />
<Button
variant="outline"
color="red"
size="sm"
onClick={onDisconnect}
>
Disconnect
</Button>
<ActionIcon
variant="default"
size="lg"
aria-label="Toggle color scheme"
onClick={onToggleTheme}
>
{colorScheme === "dark" ? "\u2600" : "\u263E"}
</ActionIcon>
</Group>
</Group>
</AppShell.Header>
<AppShell.Main>{children}</AppShell.Main>
</AppShell>
);
}

View file

@ -0,0 +1,543 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { fn } from "storybook/test";
import { Text } from "@mantine/core";
import { ConnectedView } from "./ConnectedView.js";
import { ToolsScreen } from "../../screens/ToolsScreen/ToolsScreen";
import { ResourcesScreen } from "../../screens/ResourcesScreen/ResourcesScreen";
import { PromptsScreen } from "../../screens/PromptsScreen/PromptsScreen";
import { LoggingScreen } from "../../screens/LoggingScreen/LoggingScreen";
import { TasksScreen } from "../../screens/TasksScreen/TasksScreen";
import { HistoryScreen } from "../../screens/HistoryScreen/HistoryScreen";
import type { LogEntryProps } from "../../elements/LogEntry/LogEntry";
import type { LogLevel } from "../../elements/LogEntry/LogEntry";
const allTabs = ["Tools", "Resources", "Prompts", "Logs", "Tasks", "History"];
const logMessages: { level: LogLevel; message: string; logger?: string }[] = [
{ level: "info", message: "Server started on port 3000" },
{
level: "debug",
message: "Loading configuration from /etc/mcp/config.json",
logger: "config",
},
{
level: "warning",
message: "Deprecated API endpoint called: /v1/tools",
logger: "http",
},
{ level: "info", message: "Client connected: inspector-web-ui" },
{
level: "error",
message: "Failed to read resource: file not found at /data/missing.txt",
logger: "resources",
},
{
level: "info",
message: "Tool execution completed: search_files (245ms)",
logger: "tools",
},
{
level: "debug",
message: "Parsing JSON-RPC request body",
logger: "transport",
},
{
level: "info",
message: "Listing available tools for client session",
logger: "tools",
},
{
level: "warning",
message: "Slow query detected: 1200ms for resource lookup",
logger: "db",
},
{
level: "info",
message: "Resource subscription added: file:///config.json",
logger: "resources",
},
{
level: "debug",
message: "Heartbeat ping received from client",
logger: "session",
},
{
level: "error",
message: "Permission denied reading /etc/secrets.env",
logger: "resources",
},
{
level: "info",
message: "Prompt 'summarize' resolved with 2 messages",
logger: "prompts",
},
{
level: "debug",
message: "Cache hit for resource: db://users/42",
logger: "cache",
},
{
level: "info",
message: "Tool 'create_record' executed successfully in 89ms",
logger: "tools",
},
{
level: "warning",
message: "Client reconnection attempt #3",
logger: "session",
},
{
level: "info",
message: "New resource detected: file:///data/output.csv",
logger: "resources",
},
{
level: "debug",
message: "Serializing response payload (2.4KB)",
logger: "transport",
},
{
level: "error",
message: "Timeout waiting for tool response after 30s",
logger: "tools",
},
{
level: "info",
message: "Session initialized with capabilities: tools, resources, prompts",
},
{
level: "debug",
message: "Validating input schema for tool 'batch_process'",
logger: "tools",
},
{
level: "info",
message: "Resource template resolved: db://tables/users/rows/15",
logger: "resources",
},
{
level: "warning",
message: "Memory usage at 78% — consider increasing limits",
logger: "system",
},
{
level: "info",
message: "Client disconnected gracefully",
logger: "session",
},
{
level: "debug",
message: "Flushing log buffer to disk (128 entries)",
logger: "logging",
},
];
function pad(n: number): string {
return n.toString().padStart(2, "0");
}
const sampleLogEntries: LogEntryProps[] = Array.from({ length: 50 }, (_, i) => {
const src = logMessages[i % logMessages.length];
return {
timestamp: `2026-03-17T10:00:${pad(i + 1)}Z`,
level: src.level,
message: src.message,
logger: src.logger,
};
});
const meta: Meta<typeof ConnectedView> = {
title: "Views/ConnectedView",
component: ConnectedView,
parameters: { layout: "fullscreen" },
args: {
serverName: "my-mcp-server",
status: "connected",
latencyMs: 23,
availableTabs: allTabs,
activeTab: "Tools",
onTabChange: fn(),
onDisconnect: fn(),
onToggleTheme: fn(),
},
};
export default meta;
type Story = StoryObj<typeof ConnectedView>;
export const ToolsActive: Story = {
args: {
activeTab: "Tools",
children: (
<ToolsScreen
tools={[
{
name: "send_message",
title: "Send Message",
selected: false,
onClick: fn(),
},
{
name: "create_record",
title: "Create Record",
selected: true,
onClick: fn(),
},
{ name: "delete_records", selected: false, onClick: fn() },
{ name: "list_users", selected: false, onClick: fn() },
{ name: "batch_process", selected: false, onClick: fn() },
]}
selectedTool={{
name: "create_record",
title: "Create Record",
description: "Creates a new record with the given parameters",
schema: {
type: "object",
properties: {
title: { type: "string", description: "Record title" },
count: { type: "number", description: "Number of items" },
enabled: {
type: "boolean",
description: "Whether the record is active",
},
},
required: ["title"],
},
formValues: {},
isExecuting: false,
onFormChange: fn(),
onExecute: fn(),
onCancel: fn(),
}}
result={{
content: [
{
type: "text",
text: JSON.stringify(
{
id: 42,
title: "New Record",
count: 5,
enabled: true,
createdAt: "2026-03-17T12:00:00Z",
},
null,
2,
),
},
],
onClear: fn(),
}}
searchText=""
listChanged={false}
onSearchChange={fn()}
onRefreshList={fn()}
onSelectTool={fn()}
/>
),
},
};
export const ResourcesActive: Story = {
args: {
activeTab: "Resources",
children: (
<ResourcesScreen
resources={[
{
name: "config.json",
uri: "file:///config.json",
annotations: { audience: "developer", priority: 0.8 },
selected: true,
onClick: fn(),
},
{
name: "README.md",
uri: "file:///README.md",
selected: false,
onClick: fn(),
},
{
name: "schema.sql",
uri: "file:///schema.sql",
annotations: { priority: 0.5 },
selected: false,
onClick: fn(),
},
]}
templates={[
{
template: "file:///users/{userId}/profile",
variables: { userId: "" },
onVariableChange: fn(),
onSubmit: fn(),
},
]}
subscriptions={[
{ name: "config.json", lastUpdated: "2026-03-17T10:30:00Z" },
]}
selectedResource={{
uri: "file:///config.json",
mimeType: "application/json",
annotations: { audience: "developer", priority: 0.8 },
content: JSON.stringify(
{
name: "my-project",
version: "1.0.0",
settings: { debug: true, logLevel: "info" },
},
null,
2,
),
lastUpdated: "2026-03-17T10:30:00Z",
isSubscribed: true,
onSubscribe: fn(),
onUnsubscribe: fn(),
}}
listChanged={false}
searchText=""
onSearchChange={fn()}
onRefreshList={fn()}
onSelectResource={fn()}
/>
),
},
};
export const PromptsActive: Story = {
args: {
activeTab: "Prompts",
children: (
<PromptsScreen
promptForm={{
prompts: [
{ name: "summarize", description: "Summarize a document" },
{
name: "translate",
description: "Translate text to another language",
},
{ name: "code-review", description: "Review code for issues" },
],
selectedPrompt: "translate",
arguments: [
{
name: "text",
required: true,
description: "The text to translate",
},
{
name: "targetLanguage",
required: true,
description: "Target language code",
},
],
argumentValues: {
text: "Hello, how are you?",
targetLanguage: "es",
},
onSelectPrompt: fn(),
onArgumentChange: fn(),
onGetPrompt: fn(),
}}
messages={{
onCopyAll: fn(),
messages: [
{
role: "user",
content:
'Translate the following text to Spanish: "Hello, how are you?"',
},
{ role: "assistant", content: "Hola, como estas?" },
],
}}
listChanged={false}
onRefreshList={fn()}
/>
),
},
};
export const LoggingActive: Story = {
args: {
activeTab: "Logs",
children: (
<LoggingScreen
currentLevel="info"
onSetLevel={fn()}
onClear={fn()}
onExport={fn()}
entries={sampleLogEntries}
autoScroll={true}
onToggleAutoScroll={fn()}
onCopyAll={fn()}
/>
),
},
};
export const TasksActive: Story = {
args: {
activeTab: "Tasks",
children: (
<TasksScreen
activeTasks={[
{
taskId: "task-001",
status: "running",
method: "tools/call",
target: "batch_process",
progress: 65,
progressDescription: "Processing records 650 of 1000...",
startedAt: "2026-03-17T10:05:00Z",
onViewDetails: fn(),
onViewResult: fn(),
onCancel: fn(),
onDismiss: fn(),
},
{
taskId: "task-002",
status: "waiting",
method: "resources/read",
target: "file:///data/large-dataset.csv",
startedAt: "2026-03-17T10:06:12Z",
onViewDetails: fn(),
onViewResult: fn(),
onCancel: fn(),
onDismiss: fn(),
},
]}
completedTasks={[
{
taskId: "task-000",
status: "completed",
method: "tools/call",
target: "send_message",
startedAt: "2026-03-17T09:58:00Z",
completedAt: "2026-03-17T09:58:02Z",
elapsed: "2s",
onViewDetails: fn(),
onViewResult: fn(),
onCancel: fn(),
onDismiss: fn(),
},
{
taskId: "task-099",
status: "failed",
method: "tools/call",
target: "delete_records",
startedAt: "2026-03-17T09:50:00Z",
completedAt: "2026-03-17T09:50:30Z",
elapsed: "30s",
error: "Timeout waiting for tool response after 30s",
onViewDetails: fn(),
onViewResult: fn(),
onCancel: fn(),
onDismiss: fn(),
},
]}
onRefresh={fn()}
onClearHistory={fn()}
/>
),
},
};
export const HistoryActive: Story = {
args: {
activeTab: "History",
children: (
<HistoryScreen
entries={[
{
timestamp: "2026-03-17T10:00:00Z",
method: "tools/call",
target: "send_message",
status: "success",
durationMs: 120,
parameters: { message: "Hello, world!" },
response: { result: "Message sent successfully" },
isPinned: false,
isListExpanded: false,
onReplay: fn(),
onTogglePin: fn(),
},
{
timestamp: "2026-03-17T10:01:00Z",
method: "resources/read",
target: "config.json",
status: "success",
durationMs: 45,
parameters: { uri: "file:///config.json" },
response: {
contents: [{ uri: "file:///config.json", text: "{}" }],
},
isPinned: false,
isListExpanded: false,
onReplay: fn(),
onTogglePin: fn(),
},
{
timestamp: "2026-03-17T10:02:00Z",
method: "tools/call",
target: "delete_records",
status: "error",
durationMs: 350,
parameters: { ids: [1, 2, 3] },
response: { error: "Permission denied" },
isPinned: false,
isListExpanded: true,
onReplay: fn(),
onTogglePin: fn(),
},
]}
pinnedEntries={[
{
timestamp: "2026-03-17T09:30:00Z",
method: "tools/list",
status: "success",
durationMs: 80,
response: { tools: ["send_message", "list_users"] },
isPinned: true,
isListExpanded: false,
onReplay: fn(),
onTogglePin: fn(),
},
{
timestamp: "2026-03-17T09:35:00Z",
method: "prompts/get",
target: "greeting",
status: "success",
durationMs: 60,
parameters: { name: "greeting" },
response: {
messages: [
{ role: "user", content: { type: "text", text: "Hello!" } },
],
},
isPinned: true,
isListExpanded: false,
onReplay: fn(),
onTogglePin: fn(),
},
]}
onClearAll={fn()}
onExport={fn()}
/>
),
},
};
export const LimitedTabs: Story = {
args: {
availableTabs: ["Tools", "Resources", "Prompts"],
children: <Text>Tools screen content</Text>,
},
};
export const LongServerName: Story = {
args: {
serverName: "my-very-long-server-name-that-might-overflow-is",
children: <Text>Tools screen content</Text>,
},
};

View file

@ -0,0 +1,46 @@
import type { ReactNode } from "react";
import { AppShell } from "@mantine/core";
import { ViewHeader } from "../../groups/ViewHeader/ViewHeader";
export interface ConnectedLayoutProps {
serverName: string;
status: "connected" | "connecting" | "failed";
latencyMs?: number;
activeTab: string;
availableTabs: string[];
onTabChange: (tab: string) => void;
onDisconnect: () => void;
onToggleTheme: () => void;
children: ReactNode;
}
export function ConnectedView({
serverName,
status,
latencyMs,
activeTab,
availableTabs,
onTabChange,
onDisconnect,
onToggleTheme,
children,
}: ConnectedLayoutProps) {
return (
<AppShell header={{ height: 60 }} padding="md">
<AppShell.Header>
<ViewHeader
connected
serverName={serverName}
status={status}
latencyMs={latencyMs}
activeTab={activeTab}
availableTabs={availableTabs}
onTabChange={onTabChange}
onDisconnect={onDisconnect}
onToggleTheme={onToggleTheme}
/>
</AppShell.Header>
<AppShell.Main>{children}</AppShell.Main>
</AppShell>
);
}

View file

@ -1,107 +0,0 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { fn } from "storybook/test";
import { Text } from "@mantine/core";
import { HomeLayout } from "./HomeLayout";
import { ServerListScreen } from "../../screens/ServerListScreen/ServerListScreen";
import type { ServerCardProps } from "../../groups/ServerCard/ServerCard";
const meta: Meta<typeof HomeLayout> = {
title: "Views/UnconnectedView",
component: HomeLayout,
parameters: { layout: "fullscreen" },
args: {
onToggleTheme: fn(),
},
};
export default meta;
type Story = StoryObj<typeof HomeLayout>;
function makeServerCallbacks(): Pick<
ServerCardProps,
| "onToggleConnection"
| "onServerInfo"
| "onSettings"
| "onEdit"
| "onClone"
| "onRemove"
> {
return {
onToggleConnection: fn(),
onServerInfo: fn(),
onSettings: fn(),
onEdit: fn(),
onClone: fn(),
onRemove: fn(),
};
}
export const Empty: Story = {
args: {
children: (
<ServerListScreen
servers={[]}
onAddManually={fn()}
onImportConfig={fn()}
onImportServerJson={fn()}
/>
),
},
};
export const WithServers: Story = {
args: {
children: (
<ServerListScreen
servers={[
{
name: "everything-server",
version: "1.0.0",
transport: "stdio",
connectionMode: "Via Proxy",
command: "npx -y @modelcontextprotocol/server-everything",
status: "connected",
canTestClientFeatures: true,
...makeServerCallbacks(),
},
{
name: "filesystem-server",
version: "0.6.2",
transport: "stdio",
connectionMode: "Via Proxy",
command:
"npx -y @modelcontextprotocol/server-filesystem /home/user",
status: "disconnected",
canTestClientFeatures: false,
...makeServerCallbacks(),
},
{
name: "remote-server",
version: "2.1.0",
transport: "http",
connectionMode: "Direct",
command: "https://api.example.com/mcp",
status: "failed",
retryCount: 3,
error: { message: "Connection timeout after 20s" },
canTestClientFeatures: false,
...makeServerCallbacks(),
},
]}
onAddManually={fn()}
onImportConfig={fn()}
onImportServerJson={fn()}
/>
),
},
};
export const MinimalContent: Story = {
args: {
children: (
<Text c="dimmed" ta="center" py="xl">
Page content goes here
</Text>
),
},
};

View file

@ -1,36 +0,0 @@
import type { ReactNode } from "react";
import {
ActionIcon,
AppShell,
Group,
Title,
useComputedColorScheme,
} from "@mantine/core";
export interface HomeLayoutProps {
children: ReactNode;
onToggleTheme: () => void;
}
export function HomeLayout({ children, onToggleTheme }: HomeLayoutProps) {
const colorScheme = useComputedColorScheme();
return (
<AppShell header={{ height: 60 }} padding="md">
<AppShell.Header>
<Group h="100%" px="md" justify="space-between">
<Title order={2}>MCP Inspector</Title>
<ActionIcon
variant="default"
size="lg"
aria-label="Toggle color scheme"
onClick={onToggleTheme}
>
{colorScheme === "dark" ? "\u2600" : "\u263E"}
</ActionIcon>
</Group>
</AppShell.Header>
<AppShell.Main>{children}</AppShell.Main>
</AppShell>
);
}

View file

@ -0,0 +1,259 @@
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 type { ServerCardProps } from "../../groups/ServerCard/ServerCard";
const meta: Meta<typeof UnconnectedView> = {
title: "Views/UnconnectedView",
component: UnconnectedView,
parameters: { layout: "fullscreen" },
args: {
onToggleTheme: fn(),
},
};
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(),
};
}
export const Empty: Story = {
args: {
children: (
<ServerListScreen
servers={[]}
onAddManually={fn()}
onImportConfig={fn()}
onImportServerJson={fn()}
/>
),
},
};
export const WithServers: Story = {
args: {
children: (
<ServerListScreen
servers={[
{
name: "everything-server",
version: "1.0.0",
transport: "stdio",
connectionMode: "Via Proxy",
command: "npx -y @modelcontextprotocol/server-everything",
status: "connected",
canTestClientFeatures: true,
...makeServerCallbacks(),
},
{
name: "filesystem-server",
version: "0.6.2",
transport: "stdio",
connectionMode: "Via Proxy",
command:
"npx -y @modelcontextprotocol/server-filesystem /home/user",
status: "disconnected",
canTestClientFeatures: false,
...makeServerCallbacks(),
},
{
name: "remote-server",
version: "2.1.0",
transport: "http",
connectionMode: "Direct",
command: "https://api.example.com/mcp",
status: "failed",
retryCount: 3,
error: { message: "Connection timeout after 20s" },
canTestClientFeatures: false,
...makeServerCallbacks(),
},
]}
onAddManually={fn()}
onImportConfig={fn()}
onImportServerJson={fn()}
/>
),
},
};
export const ManyServers: Story = {
args: {
children: (
<ServerListScreen
servers={[
{
name: "everything-server",
version: "1.0.0",
transport: "stdio",
connectionMode: "Via Proxy",
command: "npx -y @modelcontextprotocol/server-everything",
status: "connected",
canTestClientFeatures: true,
...makeServerCallbacks(),
},
{
name: "filesystem-server",
version: "0.6.2",
transport: "stdio",
connectionMode: "Via Proxy",
command:
"npx -y @modelcontextprotocol/server-filesystem /home/user",
status: "disconnected",
canTestClientFeatures: false,
...makeServerCallbacks(),
},
{
name: "remote-api-server",
version: "2.1.0",
transport: "http",
connectionMode: "Direct",
command: "https://api.example.com/mcp",
status: "connected",
canTestClientFeatures: true,
...makeServerCallbacks(),
},
{
name: "postgres-server",
version: "1.3.0",
transport: "stdio",
connectionMode: "Via Proxy",
command: "npx -y @modelcontextprotocol/server-postgres",
status: "connecting",
canTestClientFeatures: false,
...makeServerCallbacks(),
},
{
name: "github-server",
version: "0.9.1",
transport: "stdio",
connectionMode: "Via Proxy",
command: "npx -y @modelcontextprotocol/server-github",
status: "connected",
canTestClientFeatures: true,
...makeServerCallbacks(),
},
{
name: "slack-server",
version: "1.1.0",
transport: "http",
connectionMode: "Direct",
command: "https://slack-mcp.example.com/mcp",
status: "failed",
retryCount: 5,
error: { message: "Authentication failed" },
canTestClientFeatures: false,
...makeServerCallbacks(),
},
{
name: "memory-server",
version: "0.2.0",
transport: "stdio",
connectionMode: "Via Proxy",
command: "npx -y @modelcontextprotocol/server-memory",
status: "connected",
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: "connected",
canTestClientFeatures: true,
...makeServerCallbacks(),
},
{
name: "brave-search-server",
version: "1.2.0",
transport: "stdio",
connectionMode: "Via Proxy",
command: "npx -y @modelcontextprotocol/server-brave-search",
status: "failed",
retryCount: 2,
error: { message: "API key expired" },
canTestClientFeatures: false,
...makeServerCallbacks(),
},
{
name: "google-maps-server",
version: "0.8.0",
transport: "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: "connecting",
canTestClientFeatures: false,
...makeServerCallbacks(),
},
]}
onAddManually={fn()}
onImportConfig={fn()}
onImportServerJson={fn()}
/>
),
},
};
export const Connecting: Story = {
args: {
children: (
<ServerListScreen
servers={[
{
name: "everything-server",
version: "1.0.0",
transport: "stdio",
connectionMode: "Via Proxy",
command: "npx -y @modelcontextprotocol/server-everything",
status: "connecting",
canTestClientFeatures: false,
...makeServerCallbacks(),
},
]}
onAddManually={fn()}
onImportConfig={fn()}
onImportServerJson={fn()}
/>
),
},
};

View file

@ -0,0 +1,19 @@
import type { ReactNode } from "react";
import { AppShell } from "@mantine/core";
import { ViewHeader } from "../../groups/ViewHeader/ViewHeader";
export interface HomeLayoutProps {
children: ReactNode;
onToggleTheme: () => void;
}
export function UnconnectedView({ children, onToggleTheme }: HomeLayoutProps) {
return (
<AppShell header={{ height: 60 }} padding="md">
<AppShell.Header>
<ViewHeader connected={false} onToggleTheme={onToggleTheme} />
</AppShell.Header>
<AppShell.Main>{children}</AppShell.Main>
</AppShell>
);
}