formatting

This commit is contained in:
cliffhall 2026-03-17 16:43:27 -04:00
commit 4b28a69856
111 changed files with 2438 additions and 1874 deletions

35
.github/workflows/main.yml vendored Normal file
View file

@ -0,0 +1,35 @@
name: Run install, format, lint, build, and test on every push or pull request
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: '20.x'
cache: 'npm'
- name: Install dependencies
run: npm install
- name: Check formatting
run: npm run format:check
- name: Run linter
run: npm run lint
- name: Run type check
run: npm run typecheck
- name: Run Build
run: npm run build
- name: Run tests
run: npm run test

View file

@ -1,11 +1,11 @@
{
"name": "web",
"name": "@modelcontextprotocol/inspector-web",
"version": "0.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "web",
"name": "@modelcontextprotocol/inspector-web",
"version": "0.0.0",
"dependencies": {
"@emotion/react": "^11.14.0",
@ -36,6 +36,7 @@
"eslint-plugin-storybook": "^10.2.19",
"globals": "^17.4.0",
"playwright": "^1.58.2",
"prettier": "^3.8.1",
"storybook": "^10.2.19",
"typescript": "~5.9.3",
"typescript-eslint": "^8.56.1",
@ -5009,6 +5010,22 @@
"node": ">= 0.8.0"
}
},
"node_modules/prettier": {
"version": "3.8.1",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz",
"integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==",
"dev": true,
"license": "MIT",
"bin": {
"prettier": "bin/prettier.cjs"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/prettier/prettier?sponsor=1"
}
},
"node_modules/pretty-format": {
"version": "27.5.1",
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz",

View file

@ -7,6 +7,8 @@
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"format": "prettier --write src",
"format:check": "prettier --check src",
"preview": "vite preview",
"storybook": "storybook dev -p 6006",
"build-storybook": "storybook build"
@ -32,6 +34,7 @@
"globals": "^17.4.0",
"typescript": "~5.9.3",
"typescript-eslint": "^8.56.1",
"prettier": "^3.8.1",
"vite": "^8.0.0",
"storybook": "^10.2.19",
"@storybook/react-vite": "^10.2.19",

View file

@ -68,7 +68,7 @@
}
/* Dark mode overrides — tokens that need adjustment in dark scheme */
[data-mantine-color-scheme='dark'] {
[data-mantine-color-scheme="dark"] {
--inspector-surface-code: var(--mantine-color-dark-6);
--inspector-border-subtle: var(--mantine-color-dark-4);
}

View file

@ -7,15 +7,15 @@ import {
useComputedColorScheme,
ActionIcon,
Group,
} from '@mantine/core'
} from "@mantine/core";
function App() {
const { setColorScheme } = useMantineColorScheme()
const computedColorScheme = useComputedColorScheme('light')
const { setColorScheme } = useMantineColorScheme();
const computedColorScheme = useComputedColorScheme("light");
const toggleColorScheme = () => {
setColorScheme(computedColorScheme === 'dark' ? 'light' : 'dark')
}
setColorScheme(computedColorScheme === "dark" ? "light" : "dark");
};
return (
<Container size="sm" py="xl">
@ -28,7 +28,7 @@ function App() {
size="lg"
aria-label="Toggle color scheme"
>
{computedColorScheme === 'dark' ? '\u2600' : '\u263E'}
{computedColorScheme === "dark" ? "\u2600" : "\u263E"}
</ActionIcon>
</Group>
<Text c="var(--inspector-text-secondary)">
@ -36,7 +36,7 @@ function App() {
</Text>
</Stack>
</Container>
)
);
}
export default App
export default App;

View file

@ -1,52 +1,52 @@
import type { Meta, StoryObj } from '@storybook/react-vite'
import { AnnotationBadge } from './AnnotationBadge'
import type { Meta, StoryObj } from "@storybook/react-vite";
import { AnnotationBadge } from "./AnnotationBadge";
const meta: Meta<typeof AnnotationBadge> = {
title: 'Atoms/AnnotationBadge',
title: "Atoms/AnnotationBadge",
component: AnnotationBadge,
}
};
export default meta
type Story = StoryObj<typeof AnnotationBadge>
export default meta;
type Story = StoryObj<typeof AnnotationBadge>;
export const Audience: Story = {
args: {
label: 'user',
variant: 'audience',
label: "user",
variant: "audience",
},
}
};
export const ReadOnly: Story = {
args: {
label: 'read-only',
variant: 'readOnly',
label: "read-only",
variant: "readOnly",
},
}
};
export const Destructive: Story = {
args: {
label: 'destructive',
variant: 'destructive',
label: "destructive",
variant: "destructive",
},
}
};
export const LongRun: Story = {
args: {
label: 'long-run',
variant: 'longRun',
label: "long-run",
variant: "longRun",
},
}
};
export const PriorityHigh: Story = {
args: {
label: 'priority: high',
variant: 'priority',
label: "priority: high",
variant: "priority",
},
}
};
export const Default: Story = {
args: {
label: 'custom',
variant: 'default',
label: "custom",
variant: "default",
},
}
};

View file

@ -1,27 +1,39 @@
import { Badge } from '@mantine/core'
import { Badge } from "@mantine/core";
export type AnnotationVariant = 'audience' | 'readOnly' | 'destructive' | 'longRun' | 'priority' | 'default'
export type AnnotationVariant =
| "audience"
| "readOnly"
| "destructive"
| "longRun"
| "priority"
| "default";
export interface AnnotationBadgeProps {
label: string
variant?: AnnotationVariant
label: string;
variant?: AnnotationVariant;
}
const variantMap: Record<AnnotationVariant, { color: string; variant: string }> = {
audience: { color: 'gray', variant: 'outline' },
readOnly: { color: 'dark', variant: 'filled' },
destructive: { color: 'red', variant: 'outline' },
longRun: { color: 'yellow', variant: 'outline' },
priority: { color: 'orange', variant: 'light' },
default: { color: 'gray', variant: 'light' },
}
const variantMap: Record<
AnnotationVariant,
{ color: string; variant: string }
> = {
audience: { color: "gray", variant: "outline" },
readOnly: { color: "dark", variant: "filled" },
destructive: { color: "red", variant: "outline" },
longRun: { color: "yellow", variant: "outline" },
priority: { color: "orange", variant: "light" },
default: { color: "gray", variant: "light" },
};
export function AnnotationBadge({ label, variant = 'default' }: AnnotationBadgeProps) {
const { color, variant: badgeVariant } = variantMap[variant]
export function AnnotationBadge({
label,
variant = "default",
}: AnnotationBadgeProps) {
const { color, variant: badgeVariant } = variantMap[variant];
return (
<Badge color={color} variant={badgeVariant}>
{label}
</Badge>
)
);
}

View file

@ -1,8 +1,8 @@
import type { Meta, StoryObj } from '@storybook/react-vite';
import { CapabilityItem } from './CapabilityItem';
import type { Meta, StoryObj } from "@storybook/react-vite";
import { CapabilityItem } from "./CapabilityItem";
const meta: Meta<typeof CapabilityItem> = {
title: 'Atoms/CapabilityItem',
title: "Atoms/CapabilityItem",
component: CapabilityItem,
};
@ -11,14 +11,14 @@ type Story = StoryObj<typeof CapabilityItem>;
export const Supported: Story = {
args: {
name: 'Tools',
name: "Tools",
supported: true,
},
};
export const SupportedWithCount: Story = {
args: {
name: 'Tools',
name: "Tools",
supported: true,
count: 4,
},
@ -26,7 +26,7 @@ export const SupportedWithCount: Story = {
export const NotSupported: Story = {
args: {
name: 'Completions',
name: "Completions",
supported: false,
},
};

View file

@ -1,4 +1,4 @@
import { Group, Text } from '@mantine/core';
import { Group, Text } from "@mantine/core";
export interface CapabilityItemProps {
name: string;
@ -6,12 +6,18 @@ export interface CapabilityItemProps {
count?: number;
}
export function CapabilityItem({ name, supported, count }: CapabilityItemProps) {
export function CapabilityItem({
name,
supported,
count,
}: CapabilityItemProps) {
const label = count != null ? `${name} (${count})` : name;
return (
<Group gap="xs">
<Text c={supported ? 'green' : 'red'}>{supported ? '\u2713' : '\u2717'}</Text>
<Text c={supported ? "green" : "red"}>
{supported ? "\u2713" : "\u2717"}
</Text>
<Text>{label}</Text>
</Group>
);

View file

@ -1,9 +1,9 @@
import type { Meta, StoryObj } from '@storybook/react-vite';
import { fn } from 'storybook/test';
import { ConnectionToggle } from './ConnectionToggle';
import type { Meta, StoryObj } from "@storybook/react-vite";
import { fn } from "storybook/test";
import { ConnectionToggle } from "./ConnectionToggle";
const meta: Meta<typeof ConnectionToggle> = {
title: 'Atoms/ConnectionToggle',
title: "Atoms/ConnectionToggle",
component: ConnectionToggle,
args: {
onChange: fn(),

View file

@ -1,4 +1,4 @@
import { Switch } from '@mantine/core';
import { Switch } from "@mantine/core";
export interface ConnectionToggleProps {
checked: boolean;
@ -7,7 +7,12 @@ export interface ConnectionToggleProps {
onChange: (checked: boolean) => void;
}
export function ConnectionToggle({ checked, loading, disabled, onChange }: ConnectionToggleProps) {
export function ConnectionToggle({
checked,
loading,
disabled,
onChange,
}: ConnectionToggleProps) {
return (
<Switch
size="lg"

View file

@ -1,9 +1,9 @@
import type { Meta, StoryObj } from '@storybook/react-vite';
import { fn } from 'storybook/test';
import { ContentViewer } from './ContentViewer';
import type { Meta, StoryObj } from "@storybook/react-vite";
import { fn } from "storybook/test";
import { ContentViewer } from "./ContentViewer";
const meta: Meta<typeof ContentViewer> = {
title: 'Atoms/ContentViewer',
title: "Atoms/ContentViewer",
component: ContentViewer,
};
@ -12,56 +12,57 @@ type Story = StoryObj<typeof ContentViewer>;
export const PlainText: Story = {
args: {
type: 'text',
content: 'Hello, world!\nThis is plain text.',
type: "text",
content: "Hello, world!\nThis is plain text.",
},
};
export const JsonContent: Story = {
args: {
type: 'json',
content: '{"name":"my-app","version":"1.0.0","description":"Sample configuration file"}',
type: "json",
content:
'{"name":"my-app","version":"1.0.0","description":"Sample configuration file"}',
},
};
export const ImagePreview: Story = {
args: {
type: 'image',
type: "image",
content:
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==',
mimeType: 'image/png',
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==",
mimeType: "image/png",
},
};
export const LongContent: Story = {
args: {
type: 'json',
type: "json",
content: JSON.stringify({
name: 'my-app',
version: '2.5.0',
description: 'A comprehensive application configuration',
author: 'Inspector Team',
license: 'MIT',
repository: 'https://github.com/example/my-app',
name: "my-app",
version: "2.5.0",
description: "A comprehensive application configuration",
author: "Inspector Team",
license: "MIT",
repository: "https://github.com/example/my-app",
dependencies: {
react: '^18.0.0',
typescript: '^5.0.0',
mantine: '^7.0.0',
react: "^18.0.0",
typescript: "^5.0.0",
mantine: "^7.0.0",
},
devDependencies: {
vitest: '^1.0.0',
storybook: '^8.0.0',
eslint: '^9.0.0',
vitest: "^1.0.0",
storybook: "^8.0.0",
eslint: "^9.0.0",
},
scripts: {
dev: 'vite',
build: 'tsc && vite build',
test: 'vitest',
lint: 'eslint .',
dev: "vite",
build: "tsc && vite build",
test: "vitest",
lint: "eslint .",
},
settings: {
theme: 'dark',
language: 'en',
theme: "dark",
language: "en",
notifications: true,
maxRetries: 3,
},
@ -71,8 +72,8 @@ export const LongContent: Story = {
export const WithCopyButton: Story = {
args: {
type: 'text',
content: 'Hello, world!\nThis text can be copied.',
type: "text",
content: "Hello, world!\nThis text can be copied.",
onCopy: fn(),
},
};

View file

@ -1,7 +1,7 @@
import { Button, Code, Image, Stack } from '@mantine/core';
import { Button, Code, Image, Stack } from "@mantine/core";
export interface ContentViewerProps {
type: 'text' | 'json' | 'image' | 'audio';
type: "text" | "json" | "image" | "audio";
content: string;
mimeType?: string;
onCopy?: () => void;
@ -15,22 +15,27 @@ function formatJson(content: string): string {
}
}
export function ContentViewer({ type, content, mimeType, onCopy }: ContentViewerProps) {
export function ContentViewer({
type,
content,
mimeType,
onCopy,
}: ContentViewerProps) {
return (
<Stack gap="xs">
{type === 'json' && <Code block>{formatJson(content)}</Code>}
{type === 'text' && <Code block>{content}</Code>}
{type === 'image' && (
{type === "json" && <Code block>{formatJson(content)}</Code>}
{type === "text" && <Code block>{content}</Code>}
{type === "image" && (
<Image
src={`data:${mimeType || 'image/png'};base64,${content}`}
src={`data:${mimeType || "image/png"};base64,${content}`}
alt="Content preview"
maw={400}
radius="md"
/>
)}
{type === 'audio' && (
{type === "audio" && (
<audio controls>
<source src={`data:${mimeType || 'audio/wav'};base64,${content}`} />
<source src={`data:${mimeType || "audio/wav"};base64,${content}`} />
</audio>
)}
{onCopy && (

View file

@ -1,8 +1,8 @@
import type { Meta, StoryObj } from '@storybook/react-vite';
import { CopyButton } from './CopyButton';
import type { Meta, StoryObj } from "@storybook/react-vite";
import { CopyButton } from "./CopyButton";
const meta: Meta<typeof CopyButton> = {
title: 'Atoms/CopyButton',
title: "Atoms/CopyButton",
component: CopyButton,
};
@ -11,13 +11,13 @@ type Story = StoryObj<typeof CopyButton>;
export const Default: Story = {
args: {
value: 'Hello, world!',
value: "Hello, world!",
},
};
export const LongValue: Story = {
args: {
value:
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.',
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.",
},
};

View file

@ -1,4 +1,8 @@
import { ActionIcon, CopyButton as MantineCopyButton, Tooltip } from '@mantine/core';
import {
ActionIcon,
CopyButton as MantineCopyButton,
Tooltip,
} from "@mantine/core";
export interface CopyButtonProps {
value: string;
@ -8,9 +12,13 @@ export function CopyButton({ value }: CopyButtonProps) {
return (
<MantineCopyButton value={value}>
{({ copied, copy }) => (
<Tooltip label={copied ? 'Copied' : 'Copy'}>
<ActionIcon variant="subtle" color={copied ? 'green' : 'gray'} onClick={copy}>
{copied ? '\u2713' : '\u2398'}
<Tooltip label={copied ? "Copied" : "Copy"}>
<ActionIcon
variant="subtle"
color={copied ? "green" : "gray"}
onClick={copy}
>
{copied ? "\u2713" : "\u2398"}
</ActionIcon>
</Tooltip>
)}

View file

@ -1,8 +1,8 @@
import type { Meta, StoryObj } from '@storybook/react-vite';
import { InlineError } from './InlineError';
import type { Meta, StoryObj } from "@storybook/react-vite";
import { InlineError } from "./InlineError";
const meta: Meta<typeof InlineError> = {
title: 'Atoms/InlineError',
title: "Atoms/InlineError",
component: InlineError,
};
@ -11,13 +11,13 @@ type Story = StoryObj<typeof InlineError>;
export const ShortError: Story = {
args: {
message: 'Connection timeout after 20s',
message: "Connection timeout after 20s",
},
};
export const WithRetryCount: Story = {
args: {
message: 'Connection failed',
message: "Connection failed",
retryCount: 3,
maxRetries: 5,
},
@ -25,26 +25,26 @@ export const WithRetryCount: Story = {
export const WithDetails: Story = {
args: {
message: 'Connection refused',
message: "Connection refused",
details:
'Error: ECONNREFUSED 127.0.0.1:3000\n at TCPConnectWrap.afterConnect [as oncomplete] (node:net:1494:16)',
"Error: ECONNREFUSED 127.0.0.1:3000\n at TCPConnectWrap.afterConnect [as oncomplete] (node:net:1494:16)",
},
};
export const WithDocLink: Story = {
args: {
message: 'Connection failed',
docLink: 'https://example.com/troubleshooting',
message: "Connection failed",
docLink: "https://example.com/troubleshooting",
},
};
export const FullError: Story = {
args: {
message: 'Connection refused',
message: "Connection refused",
details:
'Error: ECONNREFUSED 127.0.0.1:3000\n at TCPConnectWrap.afterConnect [as oncomplete] (node:net:1494:16)',
"Error: ECONNREFUSED 127.0.0.1:3000\n at TCPConnectWrap.afterConnect [as oncomplete] (node:net:1494:16)",
retryCount: 3,
maxRetries: 5,
docLink: 'https://example.com/troubleshooting',
docLink: "https://example.com/troubleshooting",
},
};

View file

@ -1,4 +1,4 @@
import { Alert, Anchor, Group, Spoiler, Stack, Text } from '@mantine/core';
import { Alert, Anchor, Group, Spoiler, Stack, Text } from "@mantine/core";
export interface InlineErrorProps {
message: string;

View file

@ -1,9 +1,9 @@
import type { Meta, StoryObj } from '@storybook/react-vite';
import { fn } from 'storybook/test';
import { ListChangedIndicator } from './ListChangedIndicator';
import type { Meta, StoryObj } from "@storybook/react-vite";
import { fn } from "storybook/test";
import { ListChangedIndicator } from "./ListChangedIndicator";
const meta: Meta<typeof ListChangedIndicator> = {
title: 'Atoms/ListChangedIndicator',
title: "Atoms/ListChangedIndicator",
component: ListChangedIndicator,
};

View file

@ -1,4 +1,4 @@
import { Button, Flex, Group, Text } from '@mantine/core';
import { Button, Flex, Group, Text } from "@mantine/core";
export interface ListChangedIndicatorProps {
visible: boolean;
@ -8,17 +8,22 @@ export interface ListChangedIndicatorProps {
const Dot = Flex.withProps({
w: 8,
h: 8,
style: { borderRadius: '50%' },
bg: 'var(--inspector-status-connecting)',
style: { borderRadius: "50%" },
bg: "var(--inspector-status-connecting)",
});
export function ListChangedIndicator({ visible, onRefresh }: ListChangedIndicatorProps) {
export function ListChangedIndicator({
visible,
onRefresh,
}: ListChangedIndicatorProps) {
if (!visible) return null;
return (
<Group gap="xs">
<Dot />
<Text size="sm" c="dimmed">List updated</Text>
<Text size="sm" c="dimmed">
List updated
</Text>
<Button size="xs" variant="light" onClick={onRefresh}>
↻ Refresh
</Button>

View file

@ -1,8 +1,8 @@
import type { Meta, StoryObj } from '@storybook/react-vite';
import { LogEntry } from './LogEntry';
import type { Meta, StoryObj } from "@storybook/react-vite";
import { LogEntry } from "./LogEntry";
const meta: Meta<typeof LogEntry> = {
title: 'Atoms/LogEntry',
title: "Atoms/LogEntry",
component: LogEntry,
};
@ -11,73 +11,73 @@ type Story = StoryObj<typeof LogEntry>;
export const Debug: Story = {
args: {
timestamp: '2026-03-17T10:00:00Z',
level: 'debug',
message: 'Initializing connection pool',
timestamp: "2026-03-17T10:00:00Z",
level: "debug",
message: "Initializing connection pool",
},
};
export const Info: Story = {
args: {
timestamp: '2026-03-17T10:00:01Z',
level: 'info',
message: 'Server started on port 3000',
timestamp: "2026-03-17T10:00:01Z",
level: "info",
message: "Server started on port 3000",
},
};
export const Notice: Story = {
args: {
timestamp: '2026-03-17T10:00:02Z',
level: 'notice',
message: 'Configuration reloaded successfully',
timestamp: "2026-03-17T10:00:02Z",
level: "notice",
message: "Configuration reloaded successfully",
},
};
export const Warning: Story = {
args: {
timestamp: '2026-03-17T10:00:03Z',
level: 'warning',
message: 'Memory usage above 80%',
timestamp: "2026-03-17T10:00:03Z",
level: "warning",
message: "Memory usage above 80%",
},
};
export const Error: Story = {
args: {
timestamp: '2026-03-17T10:00:04Z',
level: 'error',
message: 'Failed to connect to database',
timestamp: "2026-03-17T10:00:04Z",
level: "error",
message: "Failed to connect to database",
},
};
export const Critical: Story = {
args: {
timestamp: '2026-03-17T10:00:05Z',
level: 'critical',
message: 'Disk space critically low',
timestamp: "2026-03-17T10:00:05Z",
level: "critical",
message: "Disk space critically low",
},
};
export const Alert: Story = {
args: {
timestamp: '2026-03-17T10:00:06Z',
level: 'alert',
message: 'Service degradation detected',
timestamp: "2026-03-17T10:00:06Z",
level: "alert",
message: "Service degradation detected",
},
};
export const Emergency: Story = {
args: {
timestamp: '2026-03-17T10:00:07Z',
level: 'emergency',
message: 'System is unusable',
timestamp: "2026-03-17T10:00:07Z",
level: "emergency",
message: "System is unusable",
},
};
export const WithLogger: Story = {
args: {
timestamp: '2026-03-17T10:00:08Z',
level: 'info',
message: 'Tool execution completed',
logger: 'mcp-server',
timestamp: "2026-03-17T10:00:08Z",
level: "info",
message: "Tool execution completed",
logger: "mcp-server",
},
};

Some files were not shown because too many files have changed in this diff Show more