Merge pull request #1221 from modelcontextprotocol/v2/phase-2-server-settings-viewshell
Phase 2: Refactor Server/Settings/ViewShell cluster to MCP SDK types
This commit is contained in:
commit
d9b2009804
32 changed files with 1790 additions and 1913 deletions
|
|
@ -1,41 +1,59 @@
|
|||
import {
|
||||
Title,
|
||||
Text,
|
||||
Container,
|
||||
Stack,
|
||||
useMantineColorScheme,
|
||||
useComputedColorScheme,
|
||||
ActionIcon,
|
||||
Container,
|
||||
Group,
|
||||
Stack,
|
||||
Text,
|
||||
Title,
|
||||
useComputedColorScheme,
|
||||
useMantineColorScheme,
|
||||
} from "@mantine/core";
|
||||
import { MdDarkMode, MdLightMode } from "react-icons/md";
|
||||
|
||||
const PageContainer = Container.withProps({
|
||||
size: "sm",
|
||||
py: "xl",
|
||||
});
|
||||
|
||||
const PageStack = Stack.withProps({
|
||||
align: "center",
|
||||
gap: "md",
|
||||
});
|
||||
|
||||
const ThemeToggle = ActionIcon.withProps({
|
||||
variant: "default",
|
||||
size: "lg",
|
||||
"aria-label": "Toggle color scheme",
|
||||
});
|
||||
|
||||
const SubtitleText = Text.withProps({
|
||||
c: "var(--inspector-text-secondary)",
|
||||
});
|
||||
|
||||
function App() {
|
||||
const { setColorScheme } = useMantineColorScheme();
|
||||
const computedColorScheme = useComputedColorScheme("light");
|
||||
const isDark = computedColorScheme === "dark";
|
||||
const ThemeIcon = isDark ? MdLightMode : MdDarkMode;
|
||||
|
||||
const toggleColorScheme = () => {
|
||||
setColorScheme(computedColorScheme === "dark" ? "light" : "dark");
|
||||
};
|
||||
function toggleColorScheme() {
|
||||
setColorScheme(isDark ? "light" : "dark");
|
||||
}
|
||||
|
||||
return (
|
||||
<Container size="sm" py="xl">
|
||||
<Stack align="center" gap="md">
|
||||
<PageContainer>
|
||||
<PageStack>
|
||||
<Group>
|
||||
<Title order={1}>MCP Inspector</Title>
|
||||
<ActionIcon
|
||||
onClick={toggleColorScheme}
|
||||
variant="default"
|
||||
size="lg"
|
||||
aria-label="Toggle color scheme"
|
||||
>
|
||||
{computedColorScheme === "dark" ? "\u2600" : "\u263E"}
|
||||
</ActionIcon>
|
||||
<ThemeToggle onClick={toggleColorScheme}>
|
||||
<ThemeIcon size={20} />
|
||||
</ThemeToggle>
|
||||
</Group>
|
||||
<Text c="var(--inspector-text-secondary)">
|
||||
<SubtitleText>
|
||||
Web client for the Model Context Protocol Inspector
|
||||
</Text>
|
||||
</Stack>
|
||||
</Container>
|
||||
</SubtitleText>
|
||||
</PageStack>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,15 @@
|
|||
import { Group, Paper, Text } from "@mantine/core";
|
||||
import { useMediaQuery } from "@mantine/hooks";
|
||||
import type { ConnectionStatus } from "@inspector/core/mcp/types.js";
|
||||
|
||||
export interface ServerStatusIndicatorProps {
|
||||
status: ConnectionStatus;
|
||||
latencyMs?: number;
|
||||
retryCount?: number;
|
||||
// Override for the default viewport-based label visibility. Useful when
|
||||
// the indicator renders inside a constrained container (sidebar, card)
|
||||
// where the global media query doesn't reflect available space.
|
||||
showLabel?: boolean;
|
||||
}
|
||||
|
||||
const statusColorVar: Record<ConnectionStatus, string> = {
|
||||
|
|
@ -41,14 +46,19 @@ export function ServerStatusIndicator({
|
|||
status,
|
||||
latencyMs,
|
||||
retryCount,
|
||||
showLabel: showLabelProp,
|
||||
}: ServerStatusIndicatorProps) {
|
||||
const wideViewport = useMediaQuery("(min-width: 1200px)");
|
||||
const showLabel = showLabelProp ?? wideViewport;
|
||||
const label = getLabel(status, latencyMs, retryCount);
|
||||
return (
|
||||
<Group gap="xs">
|
||||
<Dot
|
||||
bg={statusColorVar[status]}
|
||||
className={status === "connecting" ? "inspector-pulse" : undefined}
|
||||
title={showLabel ? undefined : label}
|
||||
/>
|
||||
<Text size="sm">{getLabel(status, latencyMs, retryCount)}</Text>
|
||||
{showLabel && <Text size="sm">{label}</Text>}
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ export interface TransportBadgeProps {
|
|||
|
||||
const transportLabel: Record<ServerType, string> = {
|
||||
stdio: "STDIO",
|
||||
sse: "SSE",
|
||||
sse: "HTTP",
|
||||
"streamable-http": "HTTP",
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,13 @@
|
|||
import type { InspectorServerJsonDraft } from "@inspector/core/mcp/types.js";
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { fn } from "storybook/test";
|
||||
import { ImportServerJsonPanel } from "./ImportServerJsonPanel";
|
||||
|
||||
const emptyDraft: InspectorServerJsonDraft = {
|
||||
rawText: "",
|
||||
envOverrides: {},
|
||||
};
|
||||
|
||||
const meta: Meta<typeof ImportServerJsonPanel> = {
|
||||
title: "Groups/ImportServerJsonPanel",
|
||||
component: ImportServerJsonPanel,
|
||||
|
|
@ -13,10 +19,8 @@ const meta: Meta<typeof ImportServerJsonPanel> = {
|
|||
onServerNameChange: fn(),
|
||||
onAddServer: fn(),
|
||||
onCancel: fn(),
|
||||
selectedPackageIndex: 0,
|
||||
envVars: [],
|
||||
serverName: "",
|
||||
validationResults: [],
|
||||
validation: [],
|
||||
},
|
||||
};
|
||||
|
||||
|
|
@ -25,28 +29,31 @@ type Story = StoryObj<typeof ImportServerJsonPanel>;
|
|||
|
||||
export const Empty: Story = {
|
||||
args: {
|
||||
jsonContent: "",
|
||||
draft: emptyDraft,
|
||||
},
|
||||
};
|
||||
|
||||
export const ValidJson: Story = {
|
||||
args: {
|
||||
jsonContent: JSON.stringify(
|
||||
{
|
||||
name: "my-mcp-server",
|
||||
version: "1.0.0",
|
||||
packages: [
|
||||
{
|
||||
registryType: "npm",
|
||||
identifier: "my-mcp-server",
|
||||
runtimeHint: "node",
|
||||
},
|
||||
],
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
validationResults: [
|
||||
draft: {
|
||||
rawText: JSON.stringify(
|
||||
{
|
||||
name: "my-mcp-server",
|
||||
version: "1.0.0",
|
||||
packages: [
|
||||
{
|
||||
registryType: "npm",
|
||||
identifier: "my-mcp-server",
|
||||
runtimeHint: "node",
|
||||
},
|
||||
],
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
envOverrides: {},
|
||||
},
|
||||
validation: [
|
||||
{ type: "success", message: "Valid server.json format detected" },
|
||||
{ type: "success", message: "Package configuration is valid" },
|
||||
{ type: "info", message: "1 package found" },
|
||||
|
|
@ -56,31 +63,38 @@ export const ValidJson: Story = {
|
|||
|
||||
export const MultiplePackages: Story = {
|
||||
args: {
|
||||
jsonContent: JSON.stringify(
|
||||
{
|
||||
name: "multi-server",
|
||||
packages: [
|
||||
{
|
||||
registryType: "npm",
|
||||
identifier: "@scope/server",
|
||||
runtimeHint: "node",
|
||||
},
|
||||
{
|
||||
registryType: "pip",
|
||||
identifier: "server-python",
|
||||
runtimeHint: "python3",
|
||||
},
|
||||
],
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
validationResults: [
|
||||
draft: {
|
||||
rawText: JSON.stringify(
|
||||
{
|
||||
name: "multi-server",
|
||||
packages: [
|
||||
{
|
||||
registryType: "npm",
|
||||
identifier: "@scope/server",
|
||||
runtimeHint: "node",
|
||||
},
|
||||
{
|
||||
registryType: "pip",
|
||||
identifier: "server-python",
|
||||
runtimeHint: "python3",
|
||||
},
|
||||
],
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
envOverrides: {},
|
||||
},
|
||||
validation: [
|
||||
{ type: "success", message: "Valid server.json format detected" },
|
||||
{ type: "info", message: "2 packages found - select one to install" },
|
||||
],
|
||||
packages: [
|
||||
{ registryType: "npm", identifier: "@scope/server", runtimeHint: "node" },
|
||||
{
|
||||
registryType: "npm",
|
||||
identifier: "@scope/server",
|
||||
runtimeHint: "node",
|
||||
},
|
||||
{
|
||||
registryType: "pip",
|
||||
identifier: "server-python",
|
||||
|
|
@ -92,25 +106,28 @@ export const MultiplePackages: Story = {
|
|||
|
||||
export const WithEnvVars: Story = {
|
||||
args: {
|
||||
jsonContent: JSON.stringify(
|
||||
{
|
||||
name: "api-server",
|
||||
packages: [
|
||||
{
|
||||
registryType: "npm",
|
||||
identifier: "api-server",
|
||||
runtimeHint: "node",
|
||||
},
|
||||
],
|
||||
envVars: [
|
||||
{ name: "API_KEY", required: true },
|
||||
{ name: "API_URL", required: false },
|
||||
],
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
validationResults: [
|
||||
draft: {
|
||||
rawText: JSON.stringify(
|
||||
{
|
||||
name: "api-server",
|
||||
packages: [
|
||||
{
|
||||
registryType: "npm",
|
||||
identifier: "api-server",
|
||||
runtimeHint: "node",
|
||||
},
|
||||
],
|
||||
envVars: [
|
||||
{ name: "API_KEY", required: true },
|
||||
{ name: "API_URL", required: false },
|
||||
],
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
envOverrides: {},
|
||||
},
|
||||
validation: [
|
||||
{ type: "success", message: "Valid server.json format detected" },
|
||||
{
|
||||
type: "warning",
|
||||
|
|
@ -136,31 +153,36 @@ export const WithEnvVars: Story = {
|
|||
|
||||
export const FullyConfigured: Story = {
|
||||
args: {
|
||||
jsonContent: JSON.stringify(
|
||||
{
|
||||
name: "full-server",
|
||||
version: "2.1.0",
|
||||
packages: [
|
||||
{
|
||||
registryType: "npm",
|
||||
identifier: "@org/full-server",
|
||||
runtimeHint: "node",
|
||||
},
|
||||
{
|
||||
registryType: "docker",
|
||||
identifier: "org/full-server:latest",
|
||||
runtimeHint: "docker",
|
||||
},
|
||||
],
|
||||
envVars: [
|
||||
{ name: "TOKEN", required: true },
|
||||
{ name: "DEBUG", required: false },
|
||||
],
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
validationResults: [
|
||||
draft: {
|
||||
rawText: JSON.stringify(
|
||||
{
|
||||
name: "full-server",
|
||||
version: "2.1.0",
|
||||
packages: [
|
||||
{
|
||||
registryType: "npm",
|
||||
identifier: "@org/full-server",
|
||||
runtimeHint: "node",
|
||||
},
|
||||
{
|
||||
registryType: "docker",
|
||||
identifier: "org/full-server:latest",
|
||||
runtimeHint: "docker",
|
||||
},
|
||||
],
|
||||
envVars: [
|
||||
{ name: "TOKEN", required: true },
|
||||
{ name: "DEBUG", required: false },
|
||||
],
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
selectedPackageIndex: 1,
|
||||
envOverrides: { TOKEN: "sk-abc123", DEBUG: "true" },
|
||||
nameOverride: "My Custom Server",
|
||||
},
|
||||
validation: [
|
||||
{ type: "success", message: "Valid server.json format detected" },
|
||||
{ type: "success", message: "All required fields present" },
|
||||
{ type: "info", message: "2 packages found" },
|
||||
|
|
@ -181,7 +203,6 @@ export const FullyConfigured: Story = {
|
|||
runtimeHint: "docker",
|
||||
},
|
||||
],
|
||||
selectedPackageIndex: 1,
|
||||
envVars: [
|
||||
{
|
||||
name: "TOKEN",
|
||||
|
|
@ -196,6 +217,5 @@ export const FullyConfigured: Story = {
|
|||
value: "true",
|
||||
},
|
||||
],
|
||||
serverName: "My Custom Server",
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import {
|
|||
Textarea,
|
||||
Title,
|
||||
} from "@mantine/core";
|
||||
import type { InspectorServerJsonDraft } from "@inspector/core/mcp/types.js";
|
||||
|
||||
export interface ValidationResult {
|
||||
type: "success" | "warning" | "info" | "error";
|
||||
|
|
@ -29,12 +30,10 @@ export interface EnvVarInfo {
|
|||
}
|
||||
|
||||
export interface ImportServerJsonPanelProps {
|
||||
jsonContent: string;
|
||||
validationResults: ValidationResult[];
|
||||
draft: InspectorServerJsonDraft;
|
||||
validation: ValidationResult[];
|
||||
packages?: PackageInfo[];
|
||||
selectedPackageIndex: number;
|
||||
envVars: EnvVarInfo[];
|
||||
serverName: string;
|
||||
onJsonChange: (content: string) => void;
|
||||
onValidate: () => void;
|
||||
onSelectPackage: (index: number) => void;
|
||||
|
|
@ -64,12 +63,10 @@ function formatPackageLabel(pkg: PackageInfo): string {
|
|||
}
|
||||
|
||||
export function ImportServerJsonPanel({
|
||||
jsonContent,
|
||||
validationResults,
|
||||
draft,
|
||||
validation,
|
||||
packages,
|
||||
selectedPackageIndex,
|
||||
envVars,
|
||||
serverName,
|
||||
onJsonChange,
|
||||
onValidate,
|
||||
onSelectPackage,
|
||||
|
|
@ -85,7 +82,7 @@ export function ImportServerJsonPanel({
|
|||
<HintText>Paste server.json content or drag and drop a file:</HintText>
|
||||
|
||||
<Textarea
|
||||
value={jsonContent}
|
||||
value={draft.rawText}
|
||||
onChange={(e) => onJsonChange(e.currentTarget.value)}
|
||||
ff="monospace"
|
||||
autosize
|
||||
|
|
@ -97,7 +94,7 @@ export function ImportServerJsonPanel({
|
|||
|
||||
<Title order={5}>Validation Results:</Title>
|
||||
|
||||
{validationResults.map((result, index) => {
|
||||
{validation.map((result, index) => {
|
||||
const { icon, color } = validationIcons[result.type];
|
||||
return (
|
||||
<Group key={index} gap="xs">
|
||||
|
|
@ -112,7 +109,7 @@ export function ImportServerJsonPanel({
|
|||
<Divider />
|
||||
<Title order={5}>Package Selection:</Title>
|
||||
<Radio.Group
|
||||
value={String(selectedPackageIndex)}
|
||||
value={String(draft.selectedPackageIndex ?? 0)}
|
||||
onChange={(value) => onSelectPackage(Number(value))}
|
||||
>
|
||||
<Stack gap="xs">
|
||||
|
|
@ -151,7 +148,7 @@ export function ImportServerJsonPanel({
|
|||
|
||||
<TextInput
|
||||
label="Server Name (optional override)"
|
||||
value={serverName}
|
||||
value={draft.nameOverride ?? ""}
|
||||
onChange={(e) => onServerNameChange(e.currentTarget.value)}
|
||||
/>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,22 +1,43 @@
|
|||
import type {
|
||||
ConnectionState,
|
||||
MCPServerConfig,
|
||||
} from "@inspector/core/mcp/types.js";
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { fn } from "storybook/test";
|
||||
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> = {
|
||||
title: "Groups/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(),
|
||||
},
|
||||
};
|
||||
|
||||
|
|
@ -25,117 +46,88 @@ type Story = StoryObj<typeof ServerCard>;
|
|||
|
||||
export const Connected: Story = {
|
||||
args: {
|
||||
id: "7f3b2a8e-1c4d-4e5f-9a0b-3c2d1e4f5a6b",
|
||||
name: "My MCP Server",
|
||||
version: "1.2.0",
|
||||
transport: "stdio",
|
||||
connectionMode: "Subprocess",
|
||||
command: "npx -y @modelcontextprotocol/server-everything",
|
||||
status: "connected",
|
||||
canTestClientFeatures: false,
|
||||
config: stdioConfig,
|
||||
info: { name: "My MCP Server", version: "1.2.0" },
|
||||
connection: connected,
|
||||
},
|
||||
};
|
||||
|
||||
export const Disconnected: Story = {
|
||||
args: {
|
||||
id: "7f3b2a8e-1c4d-4e5f-9a0b-3c2d1e4f5a6b",
|
||||
name: "My MCP Server",
|
||||
version: "1.2.0",
|
||||
transport: "stdio",
|
||||
connectionMode: "Subprocess",
|
||||
command: "npx -y @modelcontextprotocol/server-everything",
|
||||
status: "disconnected",
|
||||
canTestClientFeatures: false,
|
||||
config: stdioConfig,
|
||||
info: { name: "My MCP Server", version: "1.2.0" },
|
||||
connection: disconnected,
|
||||
},
|
||||
};
|
||||
|
||||
export const Connecting: Story = {
|
||||
args: {
|
||||
id: "7f3b2a8e-1c4d-4e5f-9a0b-3c2d1e4f5a6b",
|
||||
name: "My MCP Server",
|
||||
version: "1.2.0",
|
||||
transport: "stdio",
|
||||
connectionMode: "Subprocess",
|
||||
command: "npx -y @modelcontextprotocol/server-everything",
|
||||
status: "connecting",
|
||||
canTestClientFeatures: false,
|
||||
config: stdioConfig,
|
||||
info: { name: "My MCP Server", version: "1.2.0" },
|
||||
connection: connecting,
|
||||
},
|
||||
};
|
||||
|
||||
export const Failed: Story = {
|
||||
args: {
|
||||
id: "7f3b2a8e-1c4d-4e5f-9a0b-3c2d1e4f5a6b",
|
||||
name: "My MCP Server",
|
||||
version: "1.2.0",
|
||||
transport: "stdio",
|
||||
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,
|
||||
},
|
||||
};
|
||||
|
||||
export const WithClientFeatures: Story = {
|
||||
args: {
|
||||
name: "My MCP Server",
|
||||
version: "1.2.0",
|
||||
transport: "stdio",
|
||||
connectionMode: "Subprocess",
|
||||
command: "npx -y @modelcontextprotocol/server-everything",
|
||||
status: "connected",
|
||||
canTestClientFeatures: true,
|
||||
config: stdioConfig,
|
||||
info: { name: "My MCP Server", version: "1.2.0" },
|
||||
connection: failed,
|
||||
},
|
||||
};
|
||||
|
||||
export const LongServerName: Story = {
|
||||
args: {
|
||||
id: "a1b2c3d4-e5f6-4789-9abc-def012345678",
|
||||
name: "my-organization-super-long-experimental-mcp-server-with-many-features-v2",
|
||||
version: "1.0.0-beta.42",
|
||||
transport: "stdio",
|
||||
connectionMode: "Subprocess",
|
||||
command:
|
||||
"npx -y @my-organization/super-long-experimental-mcp-server-with-many-features-v2",
|
||||
status: "connected",
|
||||
canTestClientFeatures: true,
|
||||
config: {
|
||||
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,
|
||||
},
|
||||
};
|
||||
|
||||
export const HttpDirect: Story = {
|
||||
args: {
|
||||
id: "9e8d7c6b-5a4f-4321-bdc6-fedcba098765",
|
||||
name: "Remote API Server",
|
||||
version: "2.0.0",
|
||||
transport: "streamable-http",
|
||||
connectionMode: "Direct",
|
||||
command: "https://api.example.com/mcp",
|
||||
status: "connected",
|
||||
canTestClientFeatures: false,
|
||||
config: httpConfig,
|
||||
info: { name: "Remote API Server", version: "2.0.0" },
|
||||
connection: connected,
|
||||
},
|
||||
};
|
||||
|
||||
export const CompactConnected: Story = {
|
||||
args: {
|
||||
id: "7f3b2a8e-1c4d-4e5f-9a0b-3c2d1e4f5a6b",
|
||||
name: "My MCP Server",
|
||||
version: "1.2.0",
|
||||
transport: "stdio",
|
||||
connectionMode: "Subprocess",
|
||||
command: "npx -y @modelcontextprotocol/server-everything",
|
||||
status: "connected",
|
||||
canTestClientFeatures: false,
|
||||
config: stdioConfig,
|
||||
info: { name: "My MCP Server", version: "1.2.0" },
|
||||
connection: connected,
|
||||
compact: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const CompactDisconnected: Story = {
|
||||
args: {
|
||||
id: "7f3b2a8e-1c4d-4e5f-9a0b-3c2d1e4f5a6b",
|
||||
name: "My MCP Server",
|
||||
version: "1.2.0",
|
||||
transport: "stdio",
|
||||
connectionMode: "Subprocess",
|
||||
command: "npx -y @modelcontextprotocol/server-everything",
|
||||
status: "disconnected",
|
||||
canTestClientFeatures: false,
|
||||
config: stdioConfig,
|
||||
info: { name: "My MCP Server", version: "1.2.0" },
|
||||
connection: disconnected,
|
||||
compact: true,
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import { useEffect } from "react";
|
||||
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 {
|
||||
ConnectionStatus,
|
||||
ConnectionState,
|
||||
MCPServerConfig,
|
||||
ServerType,
|
||||
} from "@inspector/core/mcp/types.js";
|
||||
import { ServerStatusIndicator } from "../../elements/ServerStatusIndicator/ServerStatusIndicator";
|
||||
|
|
@ -11,36 +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;
|
||||
version?: string;
|
||||
transport: ServerType;
|
||||
connectionMode: string;
|
||||
command: string;
|
||||
status: ConnectionStatus;
|
||||
retryCount?: number;
|
||||
error?: { message: string; details?: string };
|
||||
canTestClientFeatures: boolean;
|
||||
config: MCPServerConfig;
|
||||
info?: Implementation;
|
||||
connection: ConnectionState;
|
||||
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({
|
||||
|
|
@ -50,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({
|
||||
|
|
@ -68,48 +69,42 @@ const RemoveButton = Button.withProps({
|
|||
color: "red.6",
|
||||
});
|
||||
|
||||
function getTransport(config: MCPServerConfig): ServerType {
|
||||
return config.type ?? "stdio";
|
||||
}
|
||||
|
||||
const TRANSPORT_DESCRIPTION: Record<ServerType, string> = {
|
||||
stdio: "Standard I/O",
|
||||
sse: "SSE (Server Sent Events) [deprecated]",
|
||||
"streamable-http": "Streamable HTTP",
|
||||
};
|
||||
|
||||
function getCommandOrUrl(config: MCPServerConfig): string {
|
||||
if (config.type === "sse" || config.type === "streamable-http") {
|
||||
return config.url;
|
||||
}
|
||||
return config.command;
|
||||
}
|
||||
|
||||
export function ServerCard({
|
||||
id,
|
||||
name,
|
||||
version,
|
||||
transport,
|
||||
connectionMode,
|
||||
command,
|
||||
status,
|
||||
retryCount,
|
||||
error,
|
||||
canTestClientFeatures,
|
||||
config,
|
||||
info,
|
||||
connection,
|
||||
activeServer,
|
||||
onSetActiveServer,
|
||||
onToggleConnection,
|
||||
onServerInfo,
|
||||
onSettings,
|
||||
onEdit,
|
||||
onClone,
|
||||
onRemove,
|
||||
onTestSampling,
|
||||
onTestElicitationForm,
|
||||
onTestElicitationUrl,
|
||||
onConfigureRoots,
|
||||
compact = false,
|
||||
}: ServerCardProps) {
|
||||
const isThisConnecting = activeServer === name;
|
||||
const isDimmed = activeServer !== undefined && activeServer !== name;
|
||||
const displayStatus = isThisConnecting ? "connecting" : status;
|
||||
|
||||
useEffect(() => {
|
||||
if (isThisConnecting) {
|
||||
onToggleConnection(true);
|
||||
}
|
||||
}, [isThisConnecting, onToggleConnection]);
|
||||
|
||||
function handleToggle(connect: boolean) {
|
||||
if (connect && !activeServer) {
|
||||
onSetActiveServer?.(name);
|
||||
} else if (!connect && activeServer && activeServer === name) {
|
||||
onSetActiveServer?.(undefined);
|
||||
onToggleConnection(false);
|
||||
}
|
||||
}
|
||||
const isDimmed = activeServer !== undefined && activeServer !== id;
|
||||
const transport = getTransport(config);
|
||||
const commandOrUrl = getCommandOrUrl(config);
|
||||
const version = info?.version;
|
||||
|
||||
return (
|
||||
<Card
|
||||
|
|
@ -119,76 +114,59 @@ 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
|
||||
status={displayStatus}
|
||||
retryCount={retryCount}
|
||||
status={connection.status}
|
||||
retryCount={connection.retryCount}
|
||||
/>
|
||||
<ConnectionToggle
|
||||
status={displayStatus}
|
||||
status={connection.status}
|
||||
disabled={isDimmed}
|
||||
onConnect={() => handleToggle(true)}
|
||||
onDisconnect={() => handleToggle(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 block={{ type: "text", text: command }} copyable />
|
||||
<ContentViewer
|
||||
block={{ type: "text", text: commandOrUrl }}
|
||||
copyable
|
||||
/>
|
||||
|
||||
{error && (
|
||||
{connection.error && (
|
||||
<InlineError
|
||||
error={{
|
||||
message: error.message,
|
||||
data: error.details,
|
||||
message: connection.error.message,
|
||||
data: connection.error.details,
|
||||
}}
|
||||
retryCount={retryCount}
|
||||
retryCount={connection.retryCount}
|
||||
/>
|
||||
)}
|
||||
|
||||
<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>
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,31 @@
|
|||
import type {
|
||||
ClientCapabilities,
|
||||
InitializeResult,
|
||||
} from "@modelcontextprotocol/sdk/types.js";
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
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> = {
|
||||
title: "Groups/ServerInfoContent",
|
||||
component: ServerInfoContent,
|
||||
|
|
@ -11,80 +36,54 @@ type Story = StoryObj<typeof ServerInfoContent>;
|
|||
|
||||
export const FullCapabilities: Story = {
|
||||
args: {
|
||||
name: "Everything Server",
|
||||
version: "2.1.0",
|
||||
protocolVersion: "2025-03-26",
|
||||
initializeResult: fullResult,
|
||||
clientCapabilities: fullClientCaps,
|
||||
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 = {
|
||||
args: {
|
||||
name: "Simple Server",
|
||||
version: "1.0.0",
|
||||
protocolVersion: "2025-03-26",
|
||||
initializeResult: {
|
||||
protocolVersion: "2025-03-26",
|
||||
serverInfo: { name: "Simple Server", version: "1.0.0" },
|
||||
capabilities: {
|
||||
tools: { listChanged: false },
|
||||
},
|
||||
},
|
||||
clientCapabilities: {
|
||||
roots: { listChanged: true },
|
||||
},
|
||||
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 = {
|
||||
args: {
|
||||
name: "Guided Server",
|
||||
version: "1.5.0",
|
||||
protocolVersion: "2025-03-26",
|
||||
initializeResult: fullResult,
|
||||
clientCapabilities: {
|
||||
roots: { listChanged: true },
|
||||
sampling: {},
|
||||
},
|
||||
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 = {
|
||||
args: {
|
||||
name: "Authenticated Server",
|
||||
version: "3.0.0",
|
||||
protocolVersion: "2025-03-26",
|
||||
initializeResult: {
|
||||
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",
|
||||
serverCapabilities: [
|
||||
{ capability: "tools", supported: true, count: 6 },
|
||||
{ capability: "resources", supported: true, count: 3 },
|
||||
],
|
||||
clientCapabilities: [
|
||||
{ capability: "roots", supported: true },
|
||||
{ capability: "sampling", supported: false },
|
||||
],
|
||||
oauthDetails: {
|
||||
oauth: {
|
||||
authUrl: "https://auth.example.com/oauth2/authorize",
|
||||
scopes: ["read", "write", "admin"],
|
||||
accessToken: "eyJhbGciOiJSUzI1NiIs...truncated",
|
||||
|
|
|
|||
|
|
@ -7,20 +7,27 @@ import {
|
|||
Text,
|
||||
Title,
|
||||
} from "@mantine/core";
|
||||
import type {
|
||||
ClientCapabilities,
|
||||
InitializeResult,
|
||||
} from "@modelcontextprotocol/sdk/types.js";
|
||||
import type { ServerType } from "@inspector/core/mcp/types.js";
|
||||
import {
|
||||
CapabilityItem,
|
||||
type CapabilityItemProps,
|
||||
type CapabilityKey,
|
||||
} from "../../elements/CapabilityItem/CapabilityItem";
|
||||
|
||||
export interface OAuthDetails {
|
||||
authUrl?: string;
|
||||
scopes?: string[];
|
||||
accessToken?: string;
|
||||
}
|
||||
|
||||
export interface ServerInfoContentProps {
|
||||
name: string;
|
||||
version: string;
|
||||
protocolVersion: string;
|
||||
transport: string;
|
||||
serverCapabilities: CapabilityItemProps[];
|
||||
clientCapabilities: CapabilityItemProps[];
|
||||
instructions?: string;
|
||||
oauthDetails?: { authUrl?: string; scopes?: string[]; accessToken?: string };
|
||||
initializeResult: InitializeResult;
|
||||
clientCapabilities: ClientCapabilities;
|
||||
transport: ServerType;
|
||||
oauth?: OAuthDetails;
|
||||
}
|
||||
|
||||
const ValueText = Text.withProps({
|
||||
|
|
@ -32,26 +39,58 @@ function formatScopes(scopes: string[]): string {
|
|||
return scopes.join(", ");
|
||||
}
|
||||
|
||||
const SERVER_CAPABILITY_KEYS: CapabilityKey[] = [
|
||||
"tools",
|
||||
"resources",
|
||||
"prompts",
|
||||
"logging",
|
||||
"completions",
|
||||
"tasks",
|
||||
"experimental",
|
||||
];
|
||||
|
||||
const CLIENT_CAPABILITY_KEYS: CapabilityKey[] = [
|
||||
"roots",
|
||||
"sampling",
|
||||
"elicitation",
|
||||
"experimental",
|
||||
];
|
||||
|
||||
function getCapabilityEntries(
|
||||
capabilities: Record<string, unknown>,
|
||||
knownKeys: CapabilityKey[],
|
||||
): { capability: CapabilityKey; supported: boolean }[] {
|
||||
return knownKeys.map((key) => ({
|
||||
capability: key,
|
||||
supported: key in capabilities && capabilities[key] != null,
|
||||
}));
|
||||
}
|
||||
|
||||
export function ServerInfoContent({
|
||||
name,
|
||||
version,
|
||||
protocolVersion,
|
||||
transport,
|
||||
serverCapabilities,
|
||||
initializeResult,
|
||||
clientCapabilities,
|
||||
instructions,
|
||||
oauthDetails,
|
||||
transport,
|
||||
oauth,
|
||||
}: ServerInfoContentProps) {
|
||||
const { serverInfo, protocolVersion, capabilities, instructions } =
|
||||
initializeResult;
|
||||
|
||||
const serverCaps = getCapabilityEntries(capabilities, SERVER_CAPABILITY_KEYS);
|
||||
const clientCaps = getCapabilityEntries(
|
||||
clientCapabilities,
|
||||
CLIENT_CAPABILITY_KEYS,
|
||||
);
|
||||
|
||||
return (
|
||||
<Stack gap="md">
|
||||
<Title order={3}>Server Information</Title>
|
||||
|
||||
<SimpleGrid cols={2}>
|
||||
<Text size="sm">Name</Text>
|
||||
<ValueText>{name}</ValueText>
|
||||
<ValueText>{serverInfo.name}</ValueText>
|
||||
|
||||
<Text size="sm">Version</Text>
|
||||
<ValueText>{version}</ValueText>
|
||||
<ValueText>{serverInfo.version ?? "—"}</ValueText>
|
||||
|
||||
<Text size="sm">Protocol</Text>
|
||||
<ValueText>{protocolVersion}</ValueText>
|
||||
|
|
@ -63,23 +102,21 @@ export function ServerInfoContent({
|
|||
<SimpleGrid cols={2}>
|
||||
<Stack gap="xs">
|
||||
<Title order={5}>Server Capabilities</Title>
|
||||
{serverCapabilities.map((cap) => (
|
||||
{serverCaps.map((cap) => (
|
||||
<CapabilityItem
|
||||
key={cap.capability}
|
||||
capability={cap.capability}
|
||||
supported={cap.supported}
|
||||
count={cap.count}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
<Stack gap="xs">
|
||||
<Title order={5}>Client Capabilities</Title>
|
||||
{clientCapabilities.map((cap) => (
|
||||
{clientCaps.map((cap) => (
|
||||
<CapabilityItem
|
||||
key={cap.capability}
|
||||
capability={cap.capability}
|
||||
supported={cap.supported}
|
||||
count={cap.count}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
|
|
@ -92,26 +129,26 @@ export function ServerInfoContent({
|
|||
</Stack>
|
||||
)}
|
||||
|
||||
{oauthDetails && (
|
||||
{oauth && (
|
||||
<Stack gap="xs">
|
||||
<Title order={5}>OAuth Details</Title>
|
||||
<Stack gap="xs">
|
||||
{oauthDetails.authUrl && (
|
||||
{oauth.authUrl && (
|
||||
<SimpleGrid cols={2}>
|
||||
<Text size="sm">Auth URL</Text>
|
||||
<Code>{oauthDetails.authUrl}</Code>
|
||||
<Code>{oauth.authUrl}</Code>
|
||||
</SimpleGrid>
|
||||
)}
|
||||
{oauthDetails.scopes && oauthDetails.scopes.length > 0 && (
|
||||
{oauth.scopes && oauth.scopes.length > 0 && (
|
||||
<SimpleGrid cols={2}>
|
||||
<Text size="sm">Scopes</Text>
|
||||
<ValueText>{formatScopes(oauthDetails.scopes)}</ValueText>
|
||||
<ValueText>{formatScopes(oauth.scopes)}</ValueText>
|
||||
</SimpleGrid>
|
||||
)}
|
||||
{oauthDetails.accessToken && (
|
||||
{oauth.accessToken && (
|
||||
<SimpleGrid cols={2}>
|
||||
<Text size="sm">Access Token</Text>
|
||||
<Code>{oauthDetails.accessToken}</Code>
|
||||
<Code>{oauth.accessToken}</Code>
|
||||
</SimpleGrid>
|
||||
)}
|
||||
</Stack>
|
||||
|
|
|
|||
|
|
@ -1,14 +1,115 @@
|
|||
import type { InspectorServerSettings } from "@inspector/core/mcp/types.js";
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { fn } from "storybook/test";
|
||||
import { ServerSettingsForm } from "./ServerSettingsForm";
|
||||
import { useArgs } from "storybook/preview-api";
|
||||
import {
|
||||
ServerSettingsForm,
|
||||
type ServerSettingsFormProps,
|
||||
} from "./ServerSettingsForm";
|
||||
|
||||
const defaultSettings: InspectorServerSettings = {
|
||||
headers: [],
|
||||
metadata: [],
|
||||
connectionTimeout: 30000,
|
||||
requestTimeout: 60000,
|
||||
};
|
||||
|
||||
function InteractiveRender(args: ServerSettingsFormProps) {
|
||||
const [, updateArgs] = useArgs<ServerSettingsFormProps>();
|
||||
|
||||
return (
|
||||
<ServerSettingsForm
|
||||
{...args}
|
||||
onExpandedSectionsChange={(sections) => {
|
||||
args.onExpandedSectionsChange(sections);
|
||||
updateArgs({ expandedSections: sections });
|
||||
}}
|
||||
onAddHeader={() => {
|
||||
args.onAddHeader();
|
||||
updateArgs({
|
||||
settings: {
|
||||
...args.settings,
|
||||
headers: [...args.settings.headers, { key: "", value: "" }],
|
||||
},
|
||||
});
|
||||
}}
|
||||
onRemoveHeader={(index) => {
|
||||
args.onRemoveHeader(index);
|
||||
updateArgs({
|
||||
settings: {
|
||||
...args.settings,
|
||||
headers: args.settings.headers.filter((_, i) => i !== index),
|
||||
},
|
||||
});
|
||||
}}
|
||||
onHeaderChange={(index, key, value) => {
|
||||
args.onHeaderChange(index, key, value);
|
||||
updateArgs({
|
||||
settings: {
|
||||
...args.settings,
|
||||
headers: args.settings.headers.map((h, i) =>
|
||||
i === index ? { key, value } : h,
|
||||
),
|
||||
},
|
||||
});
|
||||
}}
|
||||
onAddMetadata={() => {
|
||||
args.onAddMetadata();
|
||||
updateArgs({
|
||||
settings: {
|
||||
...args.settings,
|
||||
metadata: [...args.settings.metadata, { key: "", value: "" }],
|
||||
},
|
||||
});
|
||||
}}
|
||||
onRemoveMetadata={(index) => {
|
||||
args.onRemoveMetadata(index);
|
||||
updateArgs({
|
||||
settings: {
|
||||
...args.settings,
|
||||
metadata: args.settings.metadata.filter((_, i) => i !== index),
|
||||
},
|
||||
});
|
||||
}}
|
||||
onMetadataChange={(index, key, value) => {
|
||||
args.onMetadataChange(index, key, value);
|
||||
updateArgs({
|
||||
settings: {
|
||||
...args.settings,
|
||||
metadata: args.settings.metadata.map((m, i) =>
|
||||
i === index ? { key, value } : m,
|
||||
),
|
||||
},
|
||||
});
|
||||
}}
|
||||
onTimeoutChange={(field, value) => {
|
||||
args.onTimeoutChange(field, value);
|
||||
updateArgs({
|
||||
settings: { ...args.settings, [field]: value },
|
||||
});
|
||||
}}
|
||||
onOAuthChange={(oauth) => {
|
||||
args.onOAuthChange(oauth);
|
||||
updateArgs({
|
||||
settings: {
|
||||
...args.settings,
|
||||
oauthClientId: oauth.clientId,
|
||||
oauthClientSecret: oauth.clientSecret,
|
||||
oauthScopes: oauth.scopes,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const meta: Meta<typeof ServerSettingsForm> = {
|
||||
title: "Groups/ServerSettingsForm",
|
||||
component: ServerSettingsForm,
|
||||
render: InteractiveRender,
|
||||
args: {
|
||||
expandedSections: ["connectionMode"],
|
||||
expandedSections: ["headers"],
|
||||
onExpandedSectionsChange: fn(),
|
||||
onConnectionModeChange: fn(),
|
||||
onAddHeader: fn(),
|
||||
onRemoveHeader: fn(),
|
||||
onHeaderChange: fn(),
|
||||
|
|
@ -25,55 +126,49 @@ type Story = StoryObj<typeof ServerSettingsForm>;
|
|||
|
||||
export const DefaultSettings: Story = {
|
||||
args: {
|
||||
connectionMode: "proxy",
|
||||
headers: [],
|
||||
metadata: [],
|
||||
connectionTimeout: 30000,
|
||||
requestTimeout: 60000,
|
||||
settings: defaultSettings,
|
||||
},
|
||||
};
|
||||
|
||||
export const WithHeaders: Story = {
|
||||
args: {
|
||||
connectionMode: "proxy",
|
||||
headers: [
|
||||
{ key: "Authorization", value: "Bearer token-abc-123" },
|
||||
{ key: "X-Custom-Header", value: "custom-value" },
|
||||
],
|
||||
metadata: [],
|
||||
connectionTimeout: 30000,
|
||||
requestTimeout: 60000,
|
||||
settings: {
|
||||
...defaultSettings,
|
||||
headers: [
|
||||
{ key: "Authorization", value: "Bearer token-abc-123" },
|
||||
{ key: "X-Custom-Header", value: "custom-value" },
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const WithOAuth: Story = {
|
||||
args: {
|
||||
connectionMode: "direct",
|
||||
headers: [],
|
||||
metadata: [],
|
||||
connectionTimeout: 30000,
|
||||
requestTimeout: 60000,
|
||||
oauthClientId: "my-client-id",
|
||||
oauthClientSecret: "super-secret-value",
|
||||
oauthScopes: "read write admin",
|
||||
settings: {
|
||||
...defaultSettings,
|
||||
oauthClientId: "my-client-id",
|
||||
oauthClientSecret: "super-secret-value",
|
||||
oauthScopes: "read write admin",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const AllConfigured: Story = {
|
||||
args: {
|
||||
connectionMode: "proxy",
|
||||
headers: [
|
||||
{ key: "Authorization", value: "Bearer token-abc-123" },
|
||||
{ key: "X-Request-Id", value: "req-456" },
|
||||
],
|
||||
metadata: [
|
||||
{ key: "userId", value: "user-789" },
|
||||
{ key: "sessionId", value: "session-012" },
|
||||
],
|
||||
connectionTimeout: 15000,
|
||||
requestTimeout: 45000,
|
||||
oauthClientId: "my-client-id",
|
||||
oauthClientSecret: "super-secret-value",
|
||||
oauthScopes: "read write",
|
||||
settings: {
|
||||
headers: [
|
||||
{ key: "Authorization", value: "Bearer token-abc-123" },
|
||||
{ key: "X-Request-Id", value: "req-456" },
|
||||
],
|
||||
metadata: [
|
||||
{ key: "userId", value: "user-789" },
|
||||
{ key: "sessionId", value: "session-012" },
|
||||
],
|
||||
connectionTimeout: 15000,
|
||||
requestTimeout: 45000,
|
||||
oauthClientId: "my-client-id",
|
||||
oauthClientSecret: "super-secret-value",
|
||||
oauthScopes: "read write",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -4,51 +4,38 @@ import {
|
|||
Button,
|
||||
Group,
|
||||
NumberInput,
|
||||
Select,
|
||||
Stack,
|
||||
Text,
|
||||
TextInput,
|
||||
} from "@mantine/core";
|
||||
|
||||
export interface KeyValuePair {
|
||||
key: string;
|
||||
value: string;
|
||||
}
|
||||
import type {
|
||||
InspectorServerSettings,
|
||||
OAuthSettings,
|
||||
} from "@inspector/core/mcp/types.js";
|
||||
|
||||
export type ServerSettingsSection =
|
||||
| "connectionMode"
|
||||
| "headers"
|
||||
| "metadata"
|
||||
| "timeouts"
|
||||
| "oauth";
|
||||
|
||||
export interface ServerSettingsFormProps {
|
||||
connectionMode: "proxy" | "direct";
|
||||
headers: KeyValuePair[];
|
||||
metadata: KeyValuePair[];
|
||||
connectionTimeout: number;
|
||||
requestTimeout: number;
|
||||
oauthClientId?: string;
|
||||
oauthClientSecret?: string;
|
||||
oauthScopes?: string;
|
||||
settings: InspectorServerSettings;
|
||||
expandedSections: ServerSettingsSection[];
|
||||
onExpandedSectionsChange: (sections: ServerSettingsSection[]) => void;
|
||||
onConnectionModeChange: (mode: string) => 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;
|
||||
onOAuthChange: (field: string, value: string) => void;
|
||||
onTimeoutChange: (
|
||||
field: "connectionTimeout" | "requestTimeout",
|
||||
value: number,
|
||||
) => void;
|
||||
onOAuthChange: (oauth: OAuthSettings) => void;
|
||||
}
|
||||
|
||||
const CONNECTION_MODE_OPTIONS = [
|
||||
{ value: "proxy", label: "Via Proxy" },
|
||||
{ value: "direct", label: "Direct" },
|
||||
];
|
||||
|
||||
const RemoveIcon = ActionIcon.withProps({
|
||||
color: "red",
|
||||
variant: "subtle",
|
||||
|
|
@ -75,7 +62,7 @@ function KeyValueRows({
|
|||
onChange,
|
||||
onRemove,
|
||||
}: {
|
||||
items: KeyValuePair[];
|
||||
items: { key: string; value: string }[];
|
||||
onChange: (index: number, key: string, value: string) => void;
|
||||
onRemove: (index: number) => void;
|
||||
}) {
|
||||
|
|
@ -105,17 +92,9 @@ function KeyValueRows({
|
|||
}
|
||||
|
||||
export function ServerSettingsForm({
|
||||
connectionMode,
|
||||
headers,
|
||||
metadata,
|
||||
connectionTimeout,
|
||||
requestTimeout,
|
||||
oauthClientId,
|
||||
oauthClientSecret,
|
||||
oauthScopes,
|
||||
settings,
|
||||
expandedSections,
|
||||
onExpandedSectionsChange,
|
||||
onConnectionModeChange,
|
||||
onAddHeader,
|
||||
onRemoveHeader,
|
||||
onHeaderChange,
|
||||
|
|
@ -125,11 +104,21 @@ 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 {
|
||||
clientId: settings.oauthClientId ?? "",
|
||||
clientSecret: settings.oauthClientSecret ?? "",
|
||||
scopes: settings.oauthScopes ?? "",
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<Accordion
|
||||
|
|
@ -140,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={connectionMode}
|
||||
onChange={(value) => {
|
||||
if (value) onConnectionModeChange(value);
|
||||
}}
|
||||
description={
|
||||
connectionMode === "proxy"
|
||||
? "Route through inspector proxy (required for STDIO)"
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</Accordion.Panel>
|
||||
</Accordion.Item>
|
||||
|
||||
<Accordion.Item value="headers">
|
||||
<Accordion.Control>Custom Headers</Accordion.Control>
|
||||
<Accordion.Panel>
|
||||
|
|
@ -168,11 +139,11 @@ export function ServerSettingsForm({
|
|||
</HintText>
|
||||
<AddButton onClick={onAddHeader}>+ Add Header</AddButton>
|
||||
</Group>
|
||||
{headers.length === 0 ? (
|
||||
{settings.headers.length === 0 ? (
|
||||
<EmptyHint>No custom headers configured</EmptyHint>
|
||||
) : (
|
||||
<KeyValueRows
|
||||
items={headers}
|
||||
items={settings.headers}
|
||||
onChange={onHeaderChange}
|
||||
onRemove={onRemoveHeader}
|
||||
/>
|
||||
|
|
@ -191,11 +162,11 @@ export function ServerSettingsForm({
|
|||
</HintText>
|
||||
<AddButton onClick={onAddMetadata}>+ Add Metadata</AddButton>
|
||||
</Group>
|
||||
{metadata.length === 0 ? (
|
||||
{settings.metadata.length === 0 ? (
|
||||
<EmptyHint>No request metadata configured</EmptyHint>
|
||||
) : (
|
||||
<KeyValueRows
|
||||
items={metadata}
|
||||
items={settings.metadata}
|
||||
onChange={onMetadataChange}
|
||||
onRemove={onRemoveMetadata}
|
||||
/>
|
||||
|
|
@ -211,13 +182,13 @@ export function ServerSettingsForm({
|
|||
<NumberInput
|
||||
label="Connection Timeout"
|
||||
suffix=" ms"
|
||||
value={connectionTimeout}
|
||||
value={settings.connectionTimeout}
|
||||
onChange={handleTimeoutChange("connectionTimeout")}
|
||||
/>
|
||||
<NumberInput
|
||||
label="Request Timeout"
|
||||
suffix=" ms"
|
||||
value={requestTimeout}
|
||||
value={settings.requestTimeout}
|
||||
onChange={handleTimeoutChange("requestTimeout")}
|
||||
/>
|
||||
</Group>
|
||||
|
|
@ -234,21 +205,34 @@ export function ServerSettingsForm({
|
|||
</HintText>
|
||||
<TextInput
|
||||
label="Client ID"
|
||||
value={oauthClientId ?? ""}
|
||||
onChange={(e) => onOAuthChange("clientId", e.currentTarget.value)}
|
||||
value={settings.oauthClientId ?? ""}
|
||||
onChange={(e) =>
|
||||
onOAuthChange({
|
||||
...currentOAuth(),
|
||||
clientId: e.currentTarget.value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
<TextInput
|
||||
label="Client Secret"
|
||||
value={oauthClientSecret ?? ""}
|
||||
value={settings.oauthClientSecret ?? ""}
|
||||
type="password"
|
||||
onChange={(e) =>
|
||||
onOAuthChange("clientSecret", e.currentTarget.value)
|
||||
onOAuthChange({
|
||||
...currentOAuth(),
|
||||
clientSecret: e.currentTarget.value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
<TextInput
|
||||
label="Scopes"
|
||||
value={oauthScopes ?? ""}
|
||||
onChange={(e) => onOAuthChange("scopes", e.currentTarget.value)}
|
||||
value={settings.oauthScopes ?? ""}
|
||||
onChange={(e) =>
|
||||
onOAuthChange({
|
||||
...currentOAuth(),
|
||||
scopes: e.currentTarget.value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</Stack>
|
||||
</Accordion.Panel>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,72 @@
|
|||
import type { InspectorServerSettings } from "@inspector/core/mcp/types.js";
|
||||
import { AppShell } from "@mantine/core";
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { fn } from "storybook/test";
|
||||
import { useArgs } from "storybook/preview-api";
|
||||
import {
|
||||
ServerSettingsModal,
|
||||
type ServerSettingsModalProps,
|
||||
} from "./ServerSettingsModal";
|
||||
|
||||
const initialSettings: InspectorServerSettings = {
|
||||
headers: [
|
||||
{ key: "Authorization", value: "Bearer token-abc-123" },
|
||||
{ key: "X-Request-Id", value: "req-456" },
|
||||
],
|
||||
metadata: [{ key: "userId", value: "user-789" }],
|
||||
connectionTimeout: 30000,
|
||||
requestTimeout: 60000,
|
||||
oauthClientId: "my-client-id",
|
||||
oauthClientSecret: "super-secret-value",
|
||||
oauthScopes: "read write",
|
||||
};
|
||||
|
||||
function InteractiveRender(args: ServerSettingsModalProps) {
|
||||
const [, updateArgs] = useArgs<ServerSettingsModalProps>();
|
||||
|
||||
return (
|
||||
<AppShell>
|
||||
<AppShell.Main>
|
||||
<ServerSettingsModal
|
||||
{...args}
|
||||
onSettingsChange={(settings) => {
|
||||
args.onSettingsChange(settings);
|
||||
updateArgs({ settings });
|
||||
}}
|
||||
/>
|
||||
</AppShell.Main>
|
||||
</AppShell>
|
||||
);
|
||||
}
|
||||
|
||||
const meta: Meta<typeof ServerSettingsModal> = {
|
||||
title: "Groups/ServerSettingsModal",
|
||||
component: ServerSettingsModal,
|
||||
parameters: { layout: "fullscreen" },
|
||||
render: InteractiveRender,
|
||||
args: {
|
||||
opened: true,
|
||||
onClose: fn(),
|
||||
onSettingsChange: fn(),
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof ServerSettingsModal>;
|
||||
|
||||
export const FullyConfigured: Story = {
|
||||
args: {
|
||||
settings: initialSettings,
|
||||
},
|
||||
};
|
||||
|
||||
export const EmptySettings: Story = {
|
||||
args: {
|
||||
settings: {
|
||||
headers: [],
|
||||
metadata: [],
|
||||
connectionTimeout: 30000,
|
||||
requestTimeout: 60000,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
@ -0,0 +1,137 @@
|
|||
import { useState } from "react";
|
||||
import { CloseButton, Group, Modal, Stack, Title } from "@mantine/core";
|
||||
import type {
|
||||
InspectorServerSettings,
|
||||
OAuthSettings,
|
||||
} from "@inspector/core/mcp/types.js";
|
||||
import { ListToggle } from "../../elements/ListToggle/ListToggle";
|
||||
import {
|
||||
ServerSettingsForm,
|
||||
type ServerSettingsSection,
|
||||
} from "../ServerSettingsForm/ServerSettingsForm";
|
||||
|
||||
const ALL_SECTIONS: ServerSettingsSection[] = [
|
||||
"headers",
|
||||
"metadata",
|
||||
"timeouts",
|
||||
"oauth",
|
||||
];
|
||||
|
||||
export interface ServerSettingsModalProps {
|
||||
opened: boolean;
|
||||
settings: InspectorServerSettings;
|
||||
onClose: () => void;
|
||||
onSettingsChange: (settings: InspectorServerSettings) => void;
|
||||
}
|
||||
|
||||
export function ServerSettingsModal({
|
||||
opened,
|
||||
settings,
|
||||
onClose,
|
||||
onSettingsChange,
|
||||
}: ServerSettingsModalProps) {
|
||||
const [expandedSections, setExpandedSections] = useState<
|
||||
ServerSettingsSection[]
|
||||
>(["headers"]);
|
||||
|
||||
const allExpanded = expandedSections.length === ALL_SECTIONS.length;
|
||||
|
||||
function handleToggleAll() {
|
||||
setExpandedSections(allExpanded ? [] : ALL_SECTIONS);
|
||||
}
|
||||
|
||||
function handleAddHeader() {
|
||||
onSettingsChange({
|
||||
...settings,
|
||||
headers: [...settings.headers, { key: "", value: "" }],
|
||||
});
|
||||
}
|
||||
|
||||
function handleRemoveHeader(index: number) {
|
||||
onSettingsChange({
|
||||
...settings,
|
||||
headers: settings.headers.filter((_, i) => i !== index),
|
||||
});
|
||||
}
|
||||
|
||||
function handleHeaderChange(index: number, key: string, value: string) {
|
||||
const headers = settings.headers.map((h, i) =>
|
||||
i === index ? { key, value } : h,
|
||||
);
|
||||
onSettingsChange({ ...settings, headers });
|
||||
}
|
||||
|
||||
function handleAddMetadata() {
|
||||
onSettingsChange({
|
||||
...settings,
|
||||
metadata: [...settings.metadata, { key: "", value: "" }],
|
||||
});
|
||||
}
|
||||
|
||||
function handleRemoveMetadata(index: number) {
|
||||
onSettingsChange({
|
||||
...settings,
|
||||
metadata: settings.metadata.filter((_, i) => i !== index),
|
||||
});
|
||||
}
|
||||
|
||||
function handleMetadataChange(index: number, key: string, value: string) {
|
||||
const metadata = settings.metadata.map((m, i) =>
|
||||
i === index ? { key, value } : m,
|
||||
);
|
||||
onSettingsChange({ ...settings, metadata });
|
||||
}
|
||||
|
||||
function handleTimeoutChange(
|
||||
field: "connectionTimeout" | "requestTimeout",
|
||||
value: number,
|
||||
) {
|
||||
onSettingsChange({ ...settings, [field]: value });
|
||||
}
|
||||
|
||||
function handleOAuthChange(oauth: OAuthSettings) {
|
||||
onSettingsChange({
|
||||
...settings,
|
||||
oauthClientId: oauth.clientId,
|
||||
oauthClientSecret: oauth.clientSecret,
|
||||
oauthScopes: oauth.scopes,
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
opened={opened}
|
||||
onClose={onClose}
|
||||
withCloseButton={false}
|
||||
size="lg"
|
||||
centered
|
||||
>
|
||||
<Stack gap="md">
|
||||
<Group justify="space-between" wrap="nowrap">
|
||||
<ListToggle
|
||||
compact={allExpanded}
|
||||
variant="subtle"
|
||||
onToggle={handleToggleAll}
|
||||
/>
|
||||
<Title order={4} ta="center" flex={1}>
|
||||
Server Settings
|
||||
</Title>
|
||||
<CloseButton onClick={onClose} />
|
||||
</Group>
|
||||
<ServerSettingsForm
|
||||
settings={settings}
|
||||
expandedSections={expandedSections}
|
||||
onExpandedSectionsChange={setExpandedSections}
|
||||
onAddHeader={handleAddHeader}
|
||||
onRemoveHeader={handleRemoveHeader}
|
||||
onHeaderChange={handleHeaderChange}
|
||||
onAddMetadata={handleAddMetadata}
|
||||
onRemoveMetadata={handleRemoveMetadata}
|
||||
onMetadataChange={handleMetadataChange}
|
||||
onTimeoutChange={handleTimeoutChange}
|
||||
onOAuthChange={handleOAuthChange}
|
||||
/>
|
||||
</Stack>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { AppShell } from "@mantine/core";
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { fn } from "storybook/test";
|
||||
import { AppShell } from "@mantine/core";
|
||||
import { ViewHeader } from "./ViewHeader";
|
||||
|
||||
const meta: Meta<typeof ViewHeader> = {
|
||||
|
|
@ -23,7 +23,7 @@ type Story = StoryObj<typeof ViewHeader>;
|
|||
export const Connected: Story = {
|
||||
args: {
|
||||
connected: true,
|
||||
serverName: "my-mcp-server",
|
||||
serverInfo: { name: "my-mcp-server", version: "1.2.0" },
|
||||
status: "connected",
|
||||
latencyMs: 23,
|
||||
activeTab: "Tools",
|
||||
|
|
@ -31,8 +31,8 @@ export const Connected: Story = {
|
|||
"Tools",
|
||||
"Resources",
|
||||
"Prompts",
|
||||
"Logging",
|
||||
"Tasks",
|
||||
"Logs",
|
||||
"History",
|
||||
],
|
||||
onTabChange: fn(),
|
||||
|
|
|
|||
|
|
@ -5,19 +5,23 @@ import {
|
|||
Group,
|
||||
Image,
|
||||
SegmentedControl,
|
||||
Select,
|
||||
Text,
|
||||
Title,
|
||||
useComputedColorScheme,
|
||||
} from "@mantine/core";
|
||||
import { useComputedColorScheme } from "@mantine/core";
|
||||
import { MdLightMode, MdDarkMode } from "react-icons/md";
|
||||
import { useMediaQuery } from "@mantine/hooks";
|
||||
import type { Implementation } from "@modelcontextprotocol/sdk/types.js";
|
||||
import { MdLightMode, MdDarkMode, MdLinkOff } from "react-icons/md";
|
||||
import type { ConnectionStatus } from "@inspector/core/mcp/types.js";
|
||||
import { ServerStatusIndicator } from "../../elements/ServerStatusIndicator/ServerStatusIndicator";
|
||||
import mcpLogo from "../../../theme/assets/MCP.svg";
|
||||
import mcpLogoDark from "../../../theme/assets/MCP-dark.svg";
|
||||
|
||||
interface ConnectedProps {
|
||||
connected: true;
|
||||
serverName: string;
|
||||
status: "connected" | "connecting" | "error";
|
||||
serverInfo: Implementation;
|
||||
status: ConnectionStatus;
|
||||
latencyMs?: number;
|
||||
activeTab: string;
|
||||
availableTabs: string[];
|
||||
|
|
@ -37,13 +41,13 @@ const HeaderBar = Group.withProps({
|
|||
h: "100%",
|
||||
px: "md",
|
||||
wrap: "nowrap",
|
||||
gap: 0,
|
||||
gap: "md",
|
||||
});
|
||||
|
||||
const LeftSection = Group.withProps({
|
||||
gap: "md",
|
||||
wrap: "nowrap",
|
||||
w: "33.33%",
|
||||
flex: 1,
|
||||
miw: 0,
|
||||
});
|
||||
|
||||
|
|
@ -64,17 +68,20 @@ const ServerName = Text.withProps({
|
|||
fw: 600,
|
||||
size: "lg",
|
||||
truncate: "end",
|
||||
maw: "calc(100% - 40px)",
|
||||
miw: 0,
|
||||
flex: 1,
|
||||
});
|
||||
|
||||
const CenterSection = Group.withProps({
|
||||
w: "33.33%",
|
||||
justify: "center",
|
||||
wrap: "nowrap",
|
||||
flex: "0 0 auto",
|
||||
});
|
||||
|
||||
const RightSection = Group.withProps({
|
||||
gap: "sm",
|
||||
w: "33.33%",
|
||||
wrap: "nowrap",
|
||||
flex: 1,
|
||||
miw: 0,
|
||||
justify: "flex-end",
|
||||
});
|
||||
|
||||
|
|
@ -84,6 +91,13 @@ const DisconnectButton = Button.withProps({
|
|||
size: "sm",
|
||||
});
|
||||
|
||||
const DisconnectIcon = ActionIcon.withProps({
|
||||
variant: "subtle",
|
||||
c: "red",
|
||||
size: 36,
|
||||
"aria-label": "Disconnect",
|
||||
});
|
||||
|
||||
const ThemeToggle = ActionIcon.withProps({
|
||||
variant: "subtle",
|
||||
size: 36,
|
||||
|
|
@ -99,6 +113,8 @@ const UnconnectedBar = Group.withProps({
|
|||
export function ViewHeader(props: ViewHeaderProps) {
|
||||
const colorScheme = useComputedColorScheme();
|
||||
const ThemeIcon = colorScheme === "dark" ? MdLightMode : MdDarkMode;
|
||||
const showSegmented = useMediaQuery("(min-width: 992px)");
|
||||
const showDisconnectLabel = useMediaQuery("(min-width: 768px)");
|
||||
|
||||
if (!props.connected) {
|
||||
return (
|
||||
|
|
@ -120,16 +136,29 @@ export function ViewHeader(props: ViewHeaderProps) {
|
|||
<LogoLink>
|
||||
<LogoImage src={colorScheme === "dark" ? mcpLogoDark : mcpLogo} />
|
||||
</LogoLink>
|
||||
<ServerName>{props.serverName}</ServerName>
|
||||
<ServerName>{props.serverInfo.name}</ServerName>
|
||||
</LeftSection>
|
||||
|
||||
<CenterSection>
|
||||
<SegmentedControl
|
||||
value={props.activeTab}
|
||||
onChange={props.onTabChange}
|
||||
data={props.availableTabs}
|
||||
size="sm"
|
||||
/>
|
||||
{showSegmented ? (
|
||||
<SegmentedControl
|
||||
value={props.activeTab}
|
||||
onChange={props.onTabChange}
|
||||
data={props.availableTabs}
|
||||
size="sm"
|
||||
/>
|
||||
) : (
|
||||
<Select
|
||||
value={props.activeTab}
|
||||
onChange={(value) => value && props.onTabChange(value)}
|
||||
data={props.availableTabs}
|
||||
size="sm"
|
||||
allowDeselect={false}
|
||||
// Sized to comfortably fit the longest current tab label
|
||||
// ("Resources"). Revisit if longer tabs are added.
|
||||
w={140}
|
||||
/>
|
||||
)}
|
||||
</CenterSection>
|
||||
|
||||
<RightSection>
|
||||
|
|
@ -137,9 +166,15 @@ export function ViewHeader(props: ViewHeaderProps) {
|
|||
status={props.status}
|
||||
latencyMs={props.latencyMs}
|
||||
/>
|
||||
<DisconnectButton onClick={props.onDisconnect}>
|
||||
Disconnect
|
||||
</DisconnectButton>
|
||||
{showDisconnectLabel ? (
|
||||
<DisconnectButton onClick={props.onDisconnect}>
|
||||
Disconnect
|
||||
</DisconnectButton>
|
||||
) : (
|
||||
<DisconnectIcon onClick={props.onDisconnect} title="Disconnect">
|
||||
<MdLinkOff size={20} />
|
||||
</DisconnectIcon>
|
||||
)}
|
||||
<ThemeToggle onClick={props.onToggleTheme}>
|
||||
<ThemeIcon size={20} />
|
||||
</ThemeToggle>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,140 @@
|
|||
import type { LoggingLevel } from "@modelcontextprotocol/sdk/types.js";
|
||||
import type { LogEntryData } from "../../elements/LogEntry/LogEntry";
|
||||
|
||||
const logMessages: {
|
||||
level: LoggingLevel;
|
||||
data: string;
|
||||
logger?: string;
|
||||
}[] = [
|
||||
{ level: "info", data: "Server started on port 3000" },
|
||||
{
|
||||
level: "debug",
|
||||
data: "Loading configuration from /etc/mcp/config.json",
|
||||
logger: "config",
|
||||
},
|
||||
{
|
||||
level: "warning",
|
||||
data: "Deprecated API endpoint called: /v1/tools",
|
||||
logger: "http",
|
||||
},
|
||||
{ level: "info", data: "Client connected: inspector-web-ui" },
|
||||
{
|
||||
level: "error",
|
||||
data: "Failed to read resource: file not found at /data/missing.txt",
|
||||
logger: "resources",
|
||||
},
|
||||
{
|
||||
level: "info",
|
||||
data: "Tool execution completed: search_files (245ms)",
|
||||
logger: "tools",
|
||||
},
|
||||
{
|
||||
level: "debug",
|
||||
data: "Parsing JSON-RPC request body",
|
||||
logger: "transport",
|
||||
},
|
||||
{
|
||||
level: "info",
|
||||
data: "Listing available tools for client session",
|
||||
logger: "tools",
|
||||
},
|
||||
{
|
||||
level: "warning",
|
||||
data: "Slow query detected: 1200ms for resource lookup",
|
||||
logger: "db",
|
||||
},
|
||||
{
|
||||
level: "info",
|
||||
data: "Resource subscription added: file:///config.json",
|
||||
logger: "resources",
|
||||
},
|
||||
{
|
||||
level: "debug",
|
||||
data: "Heartbeat ping received from client",
|
||||
logger: "session",
|
||||
},
|
||||
{
|
||||
level: "error",
|
||||
data: "Permission denied reading /etc/secrets.env",
|
||||
logger: "resources",
|
||||
},
|
||||
{
|
||||
level: "info",
|
||||
data: "Prompt 'summarize' resolved with 2 messages",
|
||||
logger: "prompts",
|
||||
},
|
||||
{
|
||||
level: "debug",
|
||||
data: "Cache hit for resource: db://users/42",
|
||||
logger: "cache",
|
||||
},
|
||||
{
|
||||
level: "info",
|
||||
data: "Tool 'create_record' executed successfully in 89ms",
|
||||
logger: "tools",
|
||||
},
|
||||
{
|
||||
level: "warning",
|
||||
data: "Client reconnection attempt #3",
|
||||
logger: "session",
|
||||
},
|
||||
{
|
||||
level: "info",
|
||||
data: "New resource detected: file:///data/output.csv",
|
||||
logger: "resources",
|
||||
},
|
||||
{
|
||||
level: "debug",
|
||||
data: "Serializing response payload (2.4KB)",
|
||||
logger: "transport",
|
||||
},
|
||||
{
|
||||
level: "error",
|
||||
data: "Timeout waiting for tool response after 30s",
|
||||
logger: "tools",
|
||||
},
|
||||
{
|
||||
level: "info",
|
||||
data: "Session initialized with capabilities: tools, resources, prompts",
|
||||
},
|
||||
{
|
||||
level: "debug",
|
||||
data: "Validating input schema for tool 'batch_process'",
|
||||
logger: "tools",
|
||||
},
|
||||
{
|
||||
level: "info",
|
||||
data: "Resource template resolved: db://tables/users/rows/15",
|
||||
logger: "resources",
|
||||
},
|
||||
{
|
||||
level: "warning",
|
||||
data: "Memory usage at 78% — consider increasing limits",
|
||||
logger: "system",
|
||||
},
|
||||
{
|
||||
level: "info",
|
||||
data: "Client disconnected gracefully",
|
||||
logger: "session",
|
||||
},
|
||||
{
|
||||
level: "debug",
|
||||
data: "Flushing log buffer to disk (128 entries)",
|
||||
logger: "logging",
|
||||
},
|
||||
];
|
||||
|
||||
function pad(n: number): string {
|
||||
return n.toString().padStart(2, "0");
|
||||
}
|
||||
|
||||
export const mixedEntries: LogEntryData[] = Array.from(
|
||||
{ length: 50 },
|
||||
(_, i) => {
|
||||
const src = logMessages[i % logMessages.length];
|
||||
return {
|
||||
receivedAt: new Date(`2026-03-17T10:00:${pad(i + 1)}Z`),
|
||||
params: { level: src.level, data: src.data, logger: src.logger },
|
||||
};
|
||||
},
|
||||
);
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import type { LoggingLevel } from "@modelcontextprotocol/sdk/types.js";
|
||||
import { fn } from "storybook/test";
|
||||
import { LoggingScreen } from "./LoggingScreen";
|
||||
import { mixedEntries } from "./LoggingScreen.fixtures";
|
||||
import type { LogEntryData } from "../../elements/LogEntry/LogEntry";
|
||||
|
||||
const meta: Meta<typeof LoggingScreen> = {
|
||||
|
|
@ -28,141 +28,6 @@ export const Empty: Story = {
|
|||
},
|
||||
};
|
||||
|
||||
const logMessages: {
|
||||
level: LoggingLevel;
|
||||
data: string;
|
||||
logger?: string;
|
||||
}[] = [
|
||||
{ level: "info", data: "Server started on port 3000" },
|
||||
{
|
||||
level: "debug",
|
||||
data: "Loading configuration from /etc/mcp/config.json",
|
||||
logger: "config",
|
||||
},
|
||||
{
|
||||
level: "warning",
|
||||
data: "Deprecated API endpoint called: /v1/tools",
|
||||
logger: "http",
|
||||
},
|
||||
{ level: "info", data: "Client connected: inspector-web-ui" },
|
||||
{
|
||||
level: "error",
|
||||
data: "Failed to read resource: file not found at /data/missing.txt",
|
||||
logger: "resources",
|
||||
},
|
||||
{
|
||||
level: "info",
|
||||
data: "Tool execution completed: search_files (245ms)",
|
||||
logger: "tools",
|
||||
},
|
||||
{
|
||||
level: "debug",
|
||||
data: "Parsing JSON-RPC request body",
|
||||
logger: "transport",
|
||||
},
|
||||
{
|
||||
level: "info",
|
||||
data: "Listing available tools for client session",
|
||||
logger: "tools",
|
||||
},
|
||||
{
|
||||
level: "warning",
|
||||
data: "Slow query detected: 1200ms for resource lookup",
|
||||
logger: "db",
|
||||
},
|
||||
{
|
||||
level: "info",
|
||||
data: "Resource subscription added: file:///config.json",
|
||||
logger: "resources",
|
||||
},
|
||||
{
|
||||
level: "debug",
|
||||
data: "Heartbeat ping received from client",
|
||||
logger: "session",
|
||||
},
|
||||
{
|
||||
level: "error",
|
||||
data: "Permission denied reading /etc/secrets.env",
|
||||
logger: "resources",
|
||||
},
|
||||
{
|
||||
level: "info",
|
||||
data: "Prompt 'summarize' resolved with 2 messages",
|
||||
logger: "prompts",
|
||||
},
|
||||
{
|
||||
level: "debug",
|
||||
data: "Cache hit for resource: db://users/42",
|
||||
logger: "cache",
|
||||
},
|
||||
{
|
||||
level: "info",
|
||||
data: "Tool 'create_record' executed successfully in 89ms",
|
||||
logger: "tools",
|
||||
},
|
||||
{
|
||||
level: "warning",
|
||||
data: "Client reconnection attempt #3",
|
||||
logger: "session",
|
||||
},
|
||||
{
|
||||
level: "info",
|
||||
data: "New resource detected: file:///data/output.csv",
|
||||
logger: "resources",
|
||||
},
|
||||
{
|
||||
level: "debug",
|
||||
data: "Serializing response payload (2.4KB)",
|
||||
logger: "transport",
|
||||
},
|
||||
{
|
||||
level: "error",
|
||||
data: "Timeout waiting for tool response after 30s",
|
||||
logger: "tools",
|
||||
},
|
||||
{
|
||||
level: "info",
|
||||
data: "Session initialized with capabilities: tools, resources, prompts",
|
||||
},
|
||||
{
|
||||
level: "debug",
|
||||
data: "Validating input schema for tool 'batch_process'",
|
||||
logger: "tools",
|
||||
},
|
||||
{
|
||||
level: "info",
|
||||
data: "Resource template resolved: db://tables/users/rows/15",
|
||||
logger: "resources",
|
||||
},
|
||||
{
|
||||
level: "warning",
|
||||
data: "Memory usage at 78% — consider increasing limits",
|
||||
logger: "system",
|
||||
},
|
||||
{
|
||||
level: "info",
|
||||
data: "Client disconnected gracefully",
|
||||
logger: "session",
|
||||
},
|
||||
{
|
||||
level: "debug",
|
||||
data: "Flushing log buffer to disk (128 entries)",
|
||||
logger: "logging",
|
||||
},
|
||||
];
|
||||
|
||||
function pad(n: number): string {
|
||||
return n.toString().padStart(2, "0");
|
||||
}
|
||||
|
||||
const mixedEntries: LogEntryData[] = Array.from({ length: 50 }, (_, i) => {
|
||||
const src = logMessages[i % logMessages.length];
|
||||
return {
|
||||
receivedAt: new Date(`2026-03-17T10:00:${pad(i + 1)}Z`),
|
||||
params: { level: src.level, data: src.data, logger: src.logger },
|
||||
};
|
||||
});
|
||||
|
||||
export const WithEntries: Story = {
|
||||
args: {
|
||||
entries: mixedEntries,
|
||||
|
|
|
|||
|
|
@ -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,85 +10,64 @@ 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 = {
|
||||
name: "Local Dev Server",
|
||||
version: "1.2.0",
|
||||
transport: "stdio",
|
||||
connectionMode: "Direct",
|
||||
command: "npx @modelcontextprotocol/server-filesystem /home/user/projects",
|
||||
status: "connected",
|
||||
canTestClientFeatures: true,
|
||||
...makeServerCallbacks(),
|
||||
};
|
||||
|
||||
const disconnectedStdioServer: ServerCardProps = {
|
||||
name: "Database Tools",
|
||||
version: "0.9.1",
|
||||
transport: "stdio",
|
||||
connectionMode: "Direct",
|
||||
command: "python -m mcp_server_sqlite --db-path ./data.db",
|
||||
status: "disconnected",
|
||||
canTestClientFeatures: false,
|
||||
...makeServerCallbacks(),
|
||||
};
|
||||
|
||||
const failedHttpServer: ServerCardProps = {
|
||||
name: "Remote API Server",
|
||||
version: "2.0.0",
|
||||
transport: "streamable-http",
|
||||
connectionMode: "Streamable HTTP",
|
||||
command: "https://api.example.com/mcp",
|
||||
status: "error",
|
||||
retryCount: 3,
|
||||
error: {
|
||||
message: "Connection refused",
|
||||
details: "ECONNREFUSED 127.0.0.1:8080 - The server may not be running.",
|
||||
},
|
||||
canTestClientFeatures: false,
|
||||
...makeServerCallbacks(),
|
||||
};
|
||||
|
||||
const connectingHttpServer: ServerCardProps = {
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof ServerListScreen>;
|
||||
|
||||
const connectedStdioServer: ServerEntry = {
|
||||
id: "5e8c3d1f-2a4b-4c6d-8e7f-1a2b3c4d5e6f",
|
||||
name: "Local Dev Server",
|
||||
config: {
|
||||
command: "npx @modelcontextprotocol/server-filesystem /home/user/projects",
|
||||
},
|
||||
info: { name: "Local Dev Server", version: "1.2.0" },
|
||||
connection: { status: "connected" },
|
||||
};
|
||||
|
||||
const disconnectedStdioServer: ServerEntry = {
|
||||
id: "b3a7c1d2-9f8e-4a5b-bc6d-7e8f9a0b1c2d",
|
||||
name: "Database Tools",
|
||||
config: {
|
||||
command: "python -m mcp_server_sqlite --db-path ./data.db",
|
||||
},
|
||||
info: { name: "Database Tools", version: "0.9.1" },
|
||||
connection: { status: "disconnected" },
|
||||
};
|
||||
|
||||
const failedHttpServer: ServerEntry = {
|
||||
id: "c4d5e6f7-8a9b-4c0d-9e1f-2a3b4c5d6e7f",
|
||||
name: "Remote API Server",
|
||||
config: {
|
||||
type: "streamable-http",
|
||||
url: "https://api.example.com/mcp",
|
||||
},
|
||||
info: { name: "Remote API Server", version: "2.0.0" },
|
||||
connection: {
|
||||
status: "error",
|
||||
retryCount: 3,
|
||||
error: {
|
||||
message: "Connection refused",
|
||||
details: "ECONNREFUSED 127.0.0.1:8080 - The server may not be running.",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const connectingHttpServer: ServerEntry = {
|
||||
id: "d6e7f8a9-0b1c-4d2e-bf3a-4b5c6d7e8f90",
|
||||
name: "Staging Server",
|
||||
transport: "streamable-http",
|
||||
connectionMode: "Streamable HTTP",
|
||||
command: "https://staging.example.com/mcp",
|
||||
status: "connecting",
|
||||
canTestClientFeatures: false,
|
||||
...makeServerCallbacks(),
|
||||
config: {
|
||||
type: "streamable-http",
|
||||
url: "https://staging.example.com/mcp",
|
||||
},
|
||||
connection: { status: "connecting" },
|
||||
};
|
||||
|
||||
export const MultipleServers: Story = {
|
||||
|
|
@ -120,3 +98,10 @@ export const MixedStates: Story = {
|
|||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const WithActiveServer: Story = {
|
||||
args: {
|
||||
servers: [connectedStdioServer, disconnectedStdioServer, failedHttpServer],
|
||||
activeServer: connectedStdioServer.id,
|
||||
},
|
||||
};
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue