From f9a1aa4e3c10a87fb1616db7d7bcea3af69fa8d8 Mon Sep 17 00:00:00 2001 From: cliffhall Date: Thu, 16 Apr 2026 20:36:34 -0400 Subject: [PATCH 1/7] Add .claude/ to .gitignore Co-Authored-By: Claude Opus 4.6 (1M context) --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index e77e9dec..bde1cf45 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ .idea .vscode .DS_Store +.claude/ node_modules dist .env From 2d4fca4fa6571519a115129188eaf4aae78ce30d Mon Sep 17 00:00:00 2001 From: cliffhall Date: Thu, 16 Apr 2026 22:25:23 -0400 Subject: [PATCH 2/7] Phase 1: Refactor all 16 element component interfaces to use MCP SDK types MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace ad-hoc local type definitions in element components with real MCP SDK types and Inspector wrapper types from core/mcp/types.ts: Schema-typed primitives: - LogLevelBadge: LogLevel → LoggingLevel (SDK) - TransportBadge: "stdio"|"http" → ServerType (core), add sse/streamable-http - TaskStatusBadge: local TaskStatus → SDK TaskStatus (working/input_required/...) - ServerStatusIndicator: inline union → ConnectionStatus (core), "failed"→"error" MCP wrapper consumers: - AnnotationBadge: label+variant → facet+value with SDK Role type - CapabilityItem: name:string → capability:keyof ServerCapabilities|ClientCapabilities - LogEntry: flat props → entry:{receivedAt:Date, params:LoggingMessageNotification["params"]} - MessageBubble: flat props → message:SamplingMessage|PromptMessage - ProgressDisplay: flat props → params:ProgressNotification["params"] Composite-input elements: - ContentViewer: type+content+mimeType → block:ContentBlock (SDK discriminated union) - InlineError: message+details → error:{message,data?} - ConnectionToggle: checked+loading+onChange → status:ConnectionStatus+onConnect/onDisconnect Also updates all group/screen/view consumers to keep the build green, and renames CSS variable --inspector-status-failed → --inspector-status-error. Co-Authored-By: Claude Opus 4.6 (1M context) --- clients/web/src/App.css | 2 +- .../AnnotationBadge.stories.tsx | 49 ++++-- .../AnnotationBadge/AnnotationBadge.tsx | 58 ++++--- .../CapabilityItem/CapabilityItem.stories.tsx | 6 +- .../CapabilityItem/CapabilityItem.tsx | 24 ++- .../ConnectionToggle.stories.tsx | 26 ++-- .../ConnectionToggle/ConnectionToggle.tsx | 32 ++-- .../ContentViewer/ContentViewer.stories.tsx | 81 +++++----- .../elements/ContentViewer/ContentViewer.tsx | 116 +++++++++----- .../InlineError/InlineError.stories.tsx | 20 +-- .../elements/InlineError/InlineError.tsx | 15 +- .../elements/LogEntry/LogEntry.stories.tsx | 85 +++++----- .../components/elements/LogEntry/LogEntry.tsx | 51 +++--- .../elements/LogLevelBadge/LogLevelBadge.tsx | 8 +- .../MessageBubble/MessageBubble.stories.tsx | 41 +++-- .../elements/MessageBubble/MessageBubble.tsx | 73 ++++++--- .../ProgressDisplay.stories.tsx | 36 ++++- .../ProgressDisplay/ProgressDisplay.tsx | 29 ++-- .../ServerStatusIndicator.stories.tsx | 8 +- .../ServerStatusIndicator.tsx | 13 +- .../TaskStatusBadge.stories.tsx | 4 +- .../TaskStatusBadge/TaskStatusBadge.tsx | 18 ++- .../TransportBadge/TransportBadge.stories.tsx | 10 +- .../TransportBadge/TransportBadge.tsx | 11 +- .../groups/HistoryEntry/HistoryEntry.tsx | 12 +- .../LogStreamPanel/LogStreamPanel.stories.tsx | 37 +++-- .../groups/LogStreamPanel/LogStreamPanel.tsx | 18 ++- .../PromptMessagesDisplay.stories.tsx | 59 +++++-- .../PromptMessagesDisplay.tsx | 22 +-- .../ResourcePreviewPanel.tsx | 34 ++-- .../ResourceTemplatePanel.tsx | 18 +-- .../SamplingRequestPanel.stories.tsx | 49 +++++- .../SamplingRequestPanel.tsx | 15 +- .../groups/ServerCard/ServerCard.stories.tsx | 4 +- .../groups/ServerCard/ServerCard.tsx | 20 +-- .../ServerInfoContent.stories.tsx | 55 ++++--- .../ServerInfoContent/ServerInfoContent.tsx | 23 ++- .../groups/TaskCard/TaskCard.stories.tsx | 10 +- .../components/groups/TaskCard/TaskCard.tsx | 20 ++- .../TaskControls/TaskControls.stories.tsx | 2 +- .../groups/TaskControls/TaskControls.tsx | 4 +- .../TaskListPanel/TaskListPanel.stories.tsx | 4 +- .../groups/TaskListPanel/TaskListPanel.tsx | 2 +- .../ToolDetailPanel/ToolDetailPanel.tsx | 20 ++- .../ToolResultPanel/ToolResultPanel.tsx | 48 +++--- .../groups/ViewHeader/ViewHeader.tsx | 2 +- .../LoggingScreen/LoggingScreen.stories.tsx | 145 ++++++++++-------- .../screens/LoggingScreen/LoggingScreen.tsx | 4 +- .../PromptsScreen/PromptsScreen.stories.tsx | 11 +- .../ServerListScreen.stories.tsx | 6 +- .../TasksScreen/TasksScreen.stories.tsx | 8 +- .../ConnectedView/ConnectedView.stories.tsx | 79 +++++----- .../views/ConnectedView/ConnectedView.tsx | 2 +- .../UnconnectedView.stories.tsx | 8 +- 54 files changed, 907 insertions(+), 650 deletions(-) diff --git a/clients/web/src/App.css b/clients/web/src/App.css index 673797e9..48acfc34 100644 --- a/clients/web/src/App.css +++ b/clients/web/src/App.css @@ -34,7 +34,7 @@ --inspector-status-connected: var(--mantine-color-green-6); --inspector-status-connecting: var(--mantine-color-yellow-6); --inspector-status-disconnected: var(--mantine-color-gray-5); - --inspector-status-failed: var(--mantine-color-red-6); + --inspector-status-error: var(--mantine-color-red-6); /* ── Log levels (RFC 5424) ─────────────────────────────── */ --inspector-log-debug: var(--mantine-color-gray-6); diff --git a/clients/web/src/components/elements/AnnotationBadge/AnnotationBadge.stories.tsx b/clients/web/src/components/elements/AnnotationBadge/AnnotationBadge.stories.tsx index 469abc1b..4186bc2b 100644 --- a/clients/web/src/components/elements/AnnotationBadge/AnnotationBadge.stories.tsx +++ b/clients/web/src/components/elements/AnnotationBadge/AnnotationBadge.stories.tsx @@ -11,42 +11,63 @@ type Story = StoryObj; export const Audience: Story = { args: { - label: "user", - variant: "audience", + facet: "audience", + value: ["user"], + }, +}; + +export const AudienceMultiple: Story = { + args: { + facet: "audience", + value: ["user", "assistant"], }, }; export const ReadOnly: Story = { args: { - label: "read-only", - variant: "readOnly", + facet: "readOnlyHint", + value: true, }, }; export const Destructive: Story = { args: { - label: "destructive", - variant: "destructive", + facet: "destructiveHint", + value: true, }, }; -export const LongRun: Story = { +export const Idempotent: Story = { args: { - label: "long-run", - variant: "longRun", + facet: "idempotentHint", + value: true, + }, +}; + +export const OpenWorld: Story = { + args: { + facet: "openWorldHint", + value: true, + }, +}; + +export const LongRunning: Story = { + args: { + facet: "longRunHint", + value: true, }, }; export const PriorityHigh: Story = { args: { - label: "priority: high", - variant: "priority", + facet: "priority", + value: 0.9, }, }; -export const Default: Story = { +export const PriorityLow: Story = { args: { - label: "custom", - variant: "default", + facet: "priority", + value: 0.2, }, }; diff --git a/clients/web/src/components/elements/AnnotationBadge/AnnotationBadge.tsx b/clients/web/src/components/elements/AnnotationBadge/AnnotationBadge.tsx index 14b8ee29..babee2f4 100644 --- a/clients/web/src/components/elements/AnnotationBadge/AnnotationBadge.tsx +++ b/clients/web/src/components/elements/AnnotationBadge/AnnotationBadge.tsx @@ -1,37 +1,59 @@ import { Badge, useComputedColorScheme } from "@mantine/core"; +import type { Role } from "@modelcontextprotocol/sdk/types.js"; -export type AnnotationVariant = +export type AnnotationFacet = | "audience" - | "readOnly" - | "destructive" - | "longRun" | "priority" - | "default"; + | "readOnlyHint" + | "destructiveHint" + | "idempotentHint" + | "openWorldHint" + | "longRunHint"; export interface AnnotationBadgeProps { - label: string; - variant?: AnnotationVariant; + facet: AnnotationFacet; + value: Role[] | number | boolean; } -const colorMap: Record = { +const colorMap: Record = { audience: "blue", - readOnly: "green", - destructive: "red", - longRun: "yellow", priority: "orange", - default: "gray", + readOnlyHint: "green", + destructiveHint: "red", + idempotentHint: "teal", + openWorldHint: "grape", + longRunHint: "yellow", }; -export function AnnotationBadge({ - label, - variant = "default", -}: AnnotationBadgeProps) { +function formatLabel( + facet: AnnotationFacet, + value: Role[] | number | boolean, +): string { + switch (facet) { + case "audience": + return `audience: ${(value as Role[]).join(", ")}`; + case "priority": + return `priority: ${value}`; + case "readOnlyHint": + return "read-only"; + case "destructiveHint": + return "destructive"; + case "idempotentHint": + return "idempotent"; + case "openWorldHint": + return "open-world"; + case "longRunHint": + return "long-running"; + } +} + +export function AnnotationBadge({ facet, value }: AnnotationBadgeProps) { const colorScheme = useComputedColorScheme(); - const color = colorMap[variant]; + const color = colorMap[facet]; const textColor = colorScheme === "dark" ? "black" : "white"; return ( - {label} + {formatLabel(facet, value)} ); } diff --git a/clients/web/src/components/elements/CapabilityItem/CapabilityItem.stories.tsx b/clients/web/src/components/elements/CapabilityItem/CapabilityItem.stories.tsx index 19d26ae6..1c7fb425 100644 --- a/clients/web/src/components/elements/CapabilityItem/CapabilityItem.stories.tsx +++ b/clients/web/src/components/elements/CapabilityItem/CapabilityItem.stories.tsx @@ -11,14 +11,14 @@ type Story = StoryObj; export const Supported: Story = { args: { - name: "Tools", + capability: "tools", supported: true, }, }; export const SupportedWithCount: Story = { args: { - name: "Tools", + capability: "tools", supported: true, count: 4, }, @@ -26,7 +26,7 @@ export const SupportedWithCount: Story = { export const NotSupported: Story = { args: { - name: "Completions", + capability: "completions", supported: false, }, }; diff --git a/clients/web/src/components/elements/CapabilityItem/CapabilityItem.tsx b/clients/web/src/components/elements/CapabilityItem/CapabilityItem.tsx index 2a2130ef..e4a1ba63 100644 --- a/clients/web/src/components/elements/CapabilityItem/CapabilityItem.tsx +++ b/clients/web/src/components/elements/CapabilityItem/CapabilityItem.tsx @@ -1,16 +1,36 @@ import { Group, Text } from "@mantine/core"; +import type { + ClientCapabilities, + ServerCapabilities, +} from "@modelcontextprotocol/sdk/types.js"; + +export type CapabilityKey = keyof ServerCapabilities | keyof ClientCapabilities; export interface CapabilityItemProps { - name: string; + capability: CapabilityKey; supported: boolean; count?: number; } +const displayLabel: Record = { + tools: "Tools", + resources: "Resources", + prompts: "Prompts", + logging: "Logging", + completions: "Completions", + tasks: "Tasks", + experimental: "Experimental", + roots: "Roots", + sampling: "Sampling", + elicitation: "Elicitation", +}; + export function CapabilityItem({ - name, + capability, supported, count, }: CapabilityItemProps) { + const name = displayLabel[capability] ?? String(capability); const label = count != null ? `${name} (${count})` : name; return ( diff --git a/clients/web/src/components/elements/ConnectionToggle/ConnectionToggle.stories.tsx b/clients/web/src/components/elements/ConnectionToggle/ConnectionToggle.stories.tsx index eda50990..faa765db 100644 --- a/clients/web/src/components/elements/ConnectionToggle/ConnectionToggle.stories.tsx +++ b/clients/web/src/components/elements/ConnectionToggle/ConnectionToggle.stories.tsx @@ -6,7 +6,8 @@ const meta: Meta = { title: "Elements/ConnectionToggle", component: ConnectionToggle, args: { - onChange: fn(), + onConnect: fn(), + onDisconnect: fn(), }, }; @@ -15,32 +16,31 @@ type Story = StoryObj; export const Connected: Story = { args: { - checked: true, - loading: false, - disabled: false, + status: "connected", }, }; export const Disconnected: Story = { args: { - checked: false, - loading: false, - disabled: false, + status: "disconnected", }, }; -export const Loading: Story = { +export const Connecting: Story = { args: { - checked: false, - loading: true, - disabled: false, + status: "connecting", }, }; export const Disabled: Story = { args: { - checked: false, - loading: false, + status: "disconnected", disabled: true, }, }; + +export const Error: Story = { + args: { + status: "error", + }, +}; diff --git a/clients/web/src/components/elements/ConnectionToggle/ConnectionToggle.tsx b/clients/web/src/components/elements/ConnectionToggle/ConnectionToggle.tsx index 486dca61..5dcb6265 100644 --- a/clients/web/src/components/elements/ConnectionToggle/ConnectionToggle.tsx +++ b/clients/web/src/components/elements/ConnectionToggle/ConnectionToggle.tsx @@ -1,24 +1,34 @@ import { Switch } from "@mantine/core"; +import type { ConnectionStatus } from "@inspector/core/mcp/types.js"; export interface ConnectionToggleProps { - checked: boolean; - loading: boolean; - disabled: boolean; - onChange: (checked: boolean) => void; + status: ConnectionStatus; + disabled?: boolean; + onConnect: () => void; + onDisconnect: () => void; } export function ConnectionToggle({ - checked, - loading, - disabled, - onChange, + status, + disabled = false, + onConnect, + onDisconnect, }: ConnectionToggleProps) { + const isConnected = status === "connected"; + const isConnecting = status === "connecting"; + return ( onChange(event.currentTarget.checked)} + checked={isConnected || isConnecting} + disabled={disabled || isConnecting} + onChange={(event) => { + if (event.currentTarget.checked) { + onConnect(); + } else { + onDisconnect(); + } + }} /> ); } diff --git a/clients/web/src/components/elements/ContentViewer/ContentViewer.stories.tsx b/clients/web/src/components/elements/ContentViewer/ContentViewer.stories.tsx index a9dea061..4427bdd3 100644 --- a/clients/web/src/components/elements/ContentViewer/ContentViewer.stories.tsx +++ b/clients/web/src/components/elements/ContentViewer/ContentViewer.stories.tsx @@ -11,77 +11,66 @@ type Story = StoryObj; export const PlainText: Story = { args: { - type: "text", - content: "Hello, world!\nThis is plain text.", + block: { type: "text", text: "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"}', + block: { + type: "text", + text: '{"name":"my-app","version":"1.0.0","description":"Sample configuration file"}', + }, }, }; export const ImagePreview: Story = { args: { - type: "image", - content: - "iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAYAAABw4pVUAAABWUlEQVR4nO3YsQnAMAwAQe+/dLKCRZpD+UA6cVh8p/Oc81z9t1/eJ+/oD/ybVxDMKwjmFQTzCoJ5BcG8gmBeQTCvIJg3mLQX2eIVBPMKgnkFwbyCYF5BMK8gmFcQzCsI5hUE8zqdYF5BMK8gmFcQzCsI5hUE8wqCeQXBvIJgXkEwbzBpL7LFKwjmFQTzCoJ5BcG8gmBeQTCvIJhXEMwrCOZ1OsG8gmBeQTCvIJhXEMwrCOYVBPMKgnkFwbyCYN5g0l5ki1cQzCsI5hUE8wqCeQXBvIJgXkEwryCYVxDM63SCeQXBvIJgXkEwryCYVxDMKwjmFQTzCoJ5BcG8waS9yBavIJhXEMwrCOYVBPMKgnkFwbyCYF5BMK8gmNfpBPMKgnkFwbyCYF5BMK8gmFcQzCsI5hUE8wqCeYNJe5EtXkEwryCYVxDMKwjmFQTzCoJ5BcG8gmBeQTDvBaFNwZ0T5gfxAAAAAElFTkSuQmCC", - mimeType: "image/png", + block: { + type: "image", + data: "iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAYAAABw4pVUAAABWUlEQVR4nO3YsQnAMAwAQe+/dLKCRZpD+UA6cVh8p/Oc81z9t1/eJ+/oD/ybVxDMKwjmFQTzCoJ5BcG8gmBeQTCvIJg3mLQX2eIVBPMKgnkFwbyCYF5BMK8gmFcQzCsI5hUE8zqdYF5BMK8gmFcQzCsI5hUE8wqCeQXBvIJgXkEwbzBpL7LFKwjmFQTzCoJ5BcG8gmBeQTCvIJhXEMwrCOZ1OsG8gmBeQTCvIJhXEMwrCOYVBPMKgnkFwbyCYN5g0l5ki1cQzCsI5hUE8wqCeQXBvIJgXkEwryCYVxDM63SCeQXBvIJgXkEwryCYVxDMKwjmFQTzCoJ5BcG8waS9yBavIJhXEMwrCOYVBPMKgnkFwbyCYF5BMK8gmNfpBPMKgnkFwbyCYF5BMK8gmFcQzCsI5hUE8wqCeYNJe5EtXkEwryCYVxDMKwjmFQTzCoJ5BcG8gmBeQTDvBaFNwZ0T5gfxAAAAAElFTkSuQmCC", + mimeType: "image/png", + }, }, }; -export const LongContent: Story = { +export const LongJson: Story = { args: { - 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", - dependencies: { - 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", - }, - scripts: { - dev: "vite", - build: "tsc && vite build", - test: "vitest", - lint: "eslint .", - }, - settings: { - theme: "dark", - language: "en", - notifications: true, - maxRetries: 3, - }, - }), + block: { + type: "text", + text: JSON.stringify({ + name: "my-app", + version: "2.5.0", + description: "A comprehensive application configuration", + dependencies: { + react: "^18.0.0", + typescript: "^5.0.0", + }, + scripts: { + dev: "vite", + build: "tsc && vite build", + }, + }), + }, }, }; export const WithCopyButton: Story = { args: { - type: "text", - content: "Hello, world!\nThis text can be copied.", + block: { + type: "text", + text: "Hello, world!\nThis text can be copied.", + }, copyable: true, }, }; export const JsonWithCopy: Story = { args: { - type: "json", - content: - '{"name":"my-app","version":"1.0.0","description":"Sample configuration file"}', + block: { + type: "text", + text: '{"name":"my-app","version":"1.0.0","description":"Sample configuration file"}', + }, copyable: true, }, }; diff --git a/clients/web/src/components/elements/ContentViewer/ContentViewer.tsx b/clients/web/src/components/elements/ContentViewer/ContentViewer.tsx index d9b6bb02..74d32716 100644 --- a/clients/web/src/components/elements/ContentViewer/ContentViewer.tsx +++ b/clients/web/src/components/elements/ContentViewer/ContentViewer.tsx @@ -1,10 +1,9 @@ -import { Code, Flex, Image, Stack } from "@mantine/core"; +import { Code, Flex, Image, Stack, Text } from "@mantine/core"; +import type { ContentBlock } from "@modelcontextprotocol/sdk/types.js"; import { CopyButton } from "../CopyButton/CopyButton"; export interface ContentViewerProps { - type: "text" | "json" | "image" | "audio"; - content: string; - mimeType?: string; + block: ContentBlock; copyable?: boolean; } @@ -16,12 +15,24 @@ function formatJson(content: string): string { } } -function buildDataUri(mimeType: string, content: string): string { - return `data:${mimeType};base64,${content}`; +function isJsonText(block: ContentBlock): boolean { + if (block.type !== "text") return false; + if ( + "annotations" in block && + block.annotations && + "mimeType" in block.annotations + ) { + return ( + (block.annotations as Record).mimeType === + "application/json" + ); + } + const trimmed = block.text.trimStart(); + return trimmed.startsWith("{") || trimmed.startsWith("["); } -function formatContent(type: "text" | "json", content: string): string { - return type === "json" ? formatJson(content) : content; +function buildDataUri(mimeType: string, data: string): string { + return `data:${mimeType};base64,${data}`; } const ContentWrapper = Flex.withProps({ @@ -41,36 +52,61 @@ const PreviewImage = Image.withProps({ radius: "md", }); -export function ContentViewer({ - type, - content, - mimeType, - copyable = false, -}: ContentViewerProps) { - const showCopy = copyable && (type === "text" || type === "json"); - - return ( - - {(type === "json" || type === "text") && ( - - - {formatContent(type, content)} - - {showCopy && ( - - - - )} - - )} - {type === "image" && ( - - )} - {type === "audio" && ( - - )} - - ); +export function ContentViewer({ block, copyable = false }: ContentViewerProps) { + switch (block.type) { + case "text": { + const isJson = isJsonText(block); + const displayText = isJson ? formatJson(block.text) : block.text; + return ( + + + + {displayText} + + {copyable && ( + + + + )} + + + ); + } + case "image": + return ( + + + + ); + case "audio": + return ( + + + + ); + case "resource": + return ( + + + + {"text" in block.resource + ? block.resource.text + : `[blob: ${block.resource.uri}]`} + + + + ); + case "resource_link": + return ( + + + {block.name ?? block.uri} + + + ); + default: + return null; + } } diff --git a/clients/web/src/components/elements/InlineError/InlineError.stories.tsx b/clients/web/src/components/elements/InlineError/InlineError.stories.tsx index 60dbac4d..c3784109 100644 --- a/clients/web/src/components/elements/InlineError/InlineError.stories.tsx +++ b/clients/web/src/components/elements/InlineError/InlineError.stories.tsx @@ -11,13 +11,13 @@ type Story = StoryObj; export const ShortError: Story = { args: { - message: "Connection timeout after 20s", + error: { message: "Connection timeout after 20s" }, }, }; export const WithRetryCount: Story = { args: { - message: "Connection failed", + error: { message: "Connection failed" }, retryCount: 3, maxRetries: 5, }, @@ -25,24 +25,26 @@ export const WithRetryCount: Story = { export const WithDetails: Story = { args: { - message: "Connection refused", - details: - "Error: ECONNREFUSED 127.0.0.1:3000\n at TCPConnectWrap.afterConnect [as oncomplete] (node:net:1494:16)", + error: { + message: "Connection refused", + data: "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", + error: { message: "Connection failed" }, docLink: "https://example.com/troubleshooting", }, }; export const FullError: Story = { args: { - message: "Connection refused", - details: - "Error: ECONNREFUSED 127.0.0.1:3000\n at TCPConnectWrap.afterConnect [as oncomplete] (node:net:1494:16)", + error: { + message: "Connection refused", + data: "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", diff --git a/clients/web/src/components/elements/InlineError/InlineError.tsx b/clients/web/src/components/elements/InlineError/InlineError.tsx index 5b232178..7d86deb3 100644 --- a/clients/web/src/components/elements/InlineError/InlineError.tsx +++ b/clients/web/src/components/elements/InlineError/InlineError.tsx @@ -2,8 +2,7 @@ import { Alert, Button, Collapse, Group, Stack, Text } from "@mantine/core"; import { useState } from "react"; export interface InlineErrorProps { - message: string; - details?: string; + error: { message: string; data?: unknown }; retryCount?: number; maxRetries?: number; docLink?: string; @@ -49,14 +48,20 @@ function formatRetryLabel(retryCount: number, maxRetries?: number): string { : `Retry attempt ${retryCount}`; } +function formatDetails(data: unknown): string { + if (typeof data === "string") return data; + return JSON.stringify(data, null, 2); +} + export function InlineError({ - message, - details, + error, retryCount, maxRetries, docLink, }: InlineErrorProps) { const [expanded, setExpanded] = useState(false); + const details = + error.data !== undefined ? formatDetails(error.data) : undefined; const hasExpandable = details !== undefined || docLink !== undefined; return ( @@ -64,7 +69,7 @@ export function InlineError({ - {message} + {error.message} {retryCount !== undefined && ( {formatRetryLabel(retryCount, maxRetries)} )} diff --git a/clients/web/src/components/elements/LogEntry/LogEntry.stories.tsx b/clients/web/src/components/elements/LogEntry/LogEntry.stories.tsx index eb2d3711..9dc8205d 100644 --- a/clients/web/src/components/elements/LogEntry/LogEntry.stories.tsx +++ b/clients/web/src/components/elements/LogEntry/LogEntry.stories.tsx @@ -11,73 +11,70 @@ type Story = StoryObj; export const Debug: Story = { args: { - timestamp: "2026-03-17T10:00:00Z", - level: "debug", - message: "Initializing connection pool", + entry: { + receivedAt: new Date("2026-03-29T20:18:20.000Z"), + params: { level: "debug", data: "Initializing connection pool" }, + }, }, }; export const Info: Story = { args: { - 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", + entry: { + receivedAt: new Date("2026-03-29T20:18:21.000Z"), + params: { level: "info", data: "Server started on port 3000" }, + }, }, }; export const Warning: Story = { args: { - timestamp: "2026-03-17T10:00:03Z", - level: "warning", - message: "Memory usage above 80%", + entry: { + receivedAt: new Date("2026-03-29T20:18:22.000Z"), + params: { + level: "warning", + data: "Connection pool nearing capacity", + logger: "pool", + }, + }, }, }; export const Error: Story = { args: { - 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", - }, -}; - -export const Alert: Story = { - args: { - timestamp: "2026-03-17T10:00:06Z", - level: "alert", - message: "Service degradation detected", + entry: { + receivedAt: new Date("2026-03-29T20:18:23.000Z"), + params: { + level: "error", + data: "Failed to connect to database", + logger: "db", + }, + }, }, }; export const Emergency: Story = { args: { - timestamp: "2026-03-17T10:00:07Z", - level: "emergency", - message: "System is unusable", + entry: { + receivedAt: new Date("2026-03-29T20:18:24.000Z"), + params: { + level: "emergency", + data: "System is unusable", + logger: "kernel", + }, + }, }, }; -export const WithLogger: Story = { +export const WithJsonData: Story = { args: { - timestamp: "2026-03-17T10:00:08Z", - level: "info", - message: "Tool execution completed", - logger: "mcp-server", + entry: { + receivedAt: new Date("2026-03-29T20:18:25.000Z"), + params: { + level: "info", + data: { key: "value", count: 42 }, + logger: "api", + }, + }, }, }; diff --git a/clients/web/src/components/elements/LogEntry/LogEntry.tsx b/clients/web/src/components/elements/LogEntry/LogEntry.tsx index 73d66915..9f2c1e8e 100644 --- a/clients/web/src/components/elements/LogEntry/LogEntry.tsx +++ b/clients/web/src/components/elements/LogEntry/LogEntry.tsx @@ -1,24 +1,20 @@ import { Group, Text } from "@mantine/core"; +import type { + LoggingLevel, + LoggingMessageNotification, +} from "@modelcontextprotocol/sdk/types.js"; import { LogLevelBadge } from "../LogLevelBadge/LogLevelBadge"; -export type LogLevel = - | "debug" - | "info" - | "notice" - | "warning" - | "error" - | "critical" - | "alert" - | "emergency"; - -export interface LogEntryProps { - timestamp: string; - level: LogLevel; - message: string; - logger?: string; +export interface LogEntryData { + receivedAt: Date; + params: LoggingMessageNotification["params"]; } -const levelMessageColor: Record = { +export interface LogEntryProps { + entry: LogEntryData; +} + +const levelMessageColor: Record = { debug: "dimmed", info: "blue", notice: undefined, @@ -29,10 +25,20 @@ const levelMessageColor: Record = { emergency: "red", }; +function formatTimestamp(date: Date): string { + return date.toLocaleTimeString(); +} + function formatLogger(logger: string): string { return `[${logger}]`; } +function formatData(data: unknown): string { + if (data === undefined || data === null) return ""; + if (typeof data === "string") return data; + return JSON.stringify(data); +} + const TimestampText = Text.withProps({ size: "sm", ff: "monospace", @@ -45,13 +51,16 @@ const LoggerText = Text.withProps({ c: "dimmed", }); -export function LogEntry({ timestamp, level, message, logger }: LogEntryProps) { +export function LogEntry({ entry }: LogEntryProps) { + const { receivedAt, params } = entry; + const message = formatData(params.data); + return ( - {timestamp} - - {logger && {formatLogger(logger)}} - + {formatTimestamp(receivedAt)} + + {params.logger && {formatLogger(params.logger)}} + {message} diff --git a/clients/web/src/components/elements/LogLevelBadge/LogLevelBadge.tsx b/clients/web/src/components/elements/LogLevelBadge/LogLevelBadge.tsx index 3839b7f2..12168ae0 100644 --- a/clients/web/src/components/elements/LogLevelBadge/LogLevelBadge.tsx +++ b/clients/web/src/components/elements/LogLevelBadge/LogLevelBadge.tsx @@ -1,11 +1,11 @@ import { Badge, useComputedColorScheme } from "@mantine/core"; -import type { LogLevel } from "../LogEntry/LogEntry"; +import type { LoggingLevel } from "@modelcontextprotocol/sdk/types.js"; export interface LogLevelBadgeProps { - level: LogLevel; + level: LoggingLevel; } -const levelColor: Record = { +const levelColor: Record = { debug: "gray", info: "blue", notice: "teal", @@ -16,7 +16,7 @@ const levelColor: Record = { emergency: "red", }; -const boldLevels: Set = new Set(["alert", "emergency"]); +const boldLevels: Set = new Set(["alert", "emergency"]); export function LogLevelBadge({ level }: LogLevelBadgeProps) { const colorScheme = useComputedColorScheme(); diff --git a/clients/web/src/components/elements/MessageBubble/MessageBubble.stories.tsx b/clients/web/src/components/elements/MessageBubble/MessageBubble.stories.tsx index dd6b1608..44c75fde 100644 --- a/clients/web/src/components/elements/MessageBubble/MessageBubble.stories.tsx +++ b/clients/web/src/components/elements/MessageBubble/MessageBubble.stories.tsx @@ -12,28 +12,39 @@ type Story = StoryObj; export const UserText: Story = { args: { index: 0, - role: "user", - content: "Hello, my name is John and I like cats", + message: { + role: "user", + content: { type: "text", text: "Hello, my name is John and I like cats" }, + }, }, }; export const AssistantText: Story = { args: { index: 1, - role: "assistant", - content: - "Nice to meet you, John! It's wonderful that you enjoy cats. They're such fascinating and independent creatures. Do you have any cats of your own?", + message: { + role: "assistant", + content: { + type: "text", + text: "Nice to meet you, John! It's wonderful that you enjoy cats. They're such fascinating and independent creatures. Do you have any cats of your own?", + }, + }, }, }; export const UserWithImage: Story = { args: { index: 0, - role: "user", - content: "Check this out", - imageContent: { - data: "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==", - mimeType: "image/png", + message: { + role: "user", + content: [ + { type: "text", text: "Check this out" }, + { + type: "image", + data: "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==", + mimeType: "image/png", + }, + ], }, }, }; @@ -41,8 +52,12 @@ export const UserWithImage: Story = { export const LongMessage: Story = { args: { index: 0, - role: "user", - content: - "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. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo.", + message: { + role: "user", + content: { + type: "text", + text: "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.", + }, + }, }, }; diff --git a/clients/web/src/components/elements/MessageBubble/MessageBubble.tsx b/clients/web/src/components/elements/MessageBubble/MessageBubble.tsx index ee24a4f1..d1ba15ec 100644 --- a/clients/web/src/components/elements/MessageBubble/MessageBubble.tsx +++ b/clients/web/src/components/elements/MessageBubble/MessageBubble.tsx @@ -1,12 +1,13 @@ import { Group, Image, Paper, Stack, Text } from "@mantine/core"; +import type { + PromptMessage, + SamplingMessage, +} from "@modelcontextprotocol/sdk/types.js"; import { CopyButton } from "../CopyButton/CopyButton"; export interface MessageBubbleProps { index: number; - role: "user" | "assistant"; - content: string; - imageContent?: { data: string; mimeType: string }; - audioContent?: { data: string; mimeType: string }; + message: SamplingMessage | PromptMessage; } function buildDataUri(mimeType: string, data: string): string { @@ -21,6 +22,41 @@ function formatQuotedContent(content: string): string { return `"${content}"`; } +interface ContentBlockRendered { + text: string; + imageUri?: string; + audioUri?: string; + audioMime?: string; +} + +function extractContent( + message: SamplingMessage | PromptMessage, +): ContentBlockRendered { + const content = message.content; + const blocks = Array.isArray(content) ? content : [content]; + let text = ""; + let imageUri: string | undefined; + let audioUri: string | undefined; + let audioMime: string | undefined; + + for (const block of blocks) { + switch (block.type) { + case "text": + text += block.text; + break; + case "image": + imageUri = buildDataUri(block.mimeType, block.data); + break; + case "audio": + audioUri = buildDataUri(block.mimeType, block.data); + audioMime = block.mimeType; + break; + } + } + + return { text, imageUri, audioUri, audioMime }; +} + const BubbleContainer = Paper.withProps({ p: "md", radius: "md", @@ -38,32 +74,21 @@ const PreviewImage = Image.withProps({ mt: "xs", }); -export function MessageBubble({ - index, - role, - content, - imageContent, - audioContent, -}: MessageBubbleProps) { +export function MessageBubble({ index, message }: MessageBubbleProps) { + const { text, imageUri, audioUri, audioMime } = extractContent(message); + return ( - {formatRoleLabel(index, role)} - + {formatRoleLabel(index, message.role)} + {text && } - {formatQuotedContent(content)} - {imageContent && ( - - )} - {audioContent && ( + {text && {formatQuotedContent(text)}} + {imageUri && } + {audioUri && ( )} diff --git a/clients/web/src/components/elements/ProgressDisplay/ProgressDisplay.stories.tsx b/clients/web/src/components/elements/ProgressDisplay/ProgressDisplay.stories.tsx index 0e8aa20f..23bab30c 100644 --- a/clients/web/src/components/elements/ProgressDisplay/ProgressDisplay.stories.tsx +++ b/clients/web/src/components/elements/ProgressDisplay/ProgressDisplay.stories.tsx @@ -11,29 +11,51 @@ type Story = StoryObj; export const ZeroPercent: Story = { args: { - progress: 0, + params: { progressToken: "t1", progress: 0, total: 100 }, }, }; export const HalfComplete: Story = { args: { - progress: 50, - description: "Processing...", + params: { + progressToken: "t2", + progress: 50, + total: 100, + message: "Processing...", + }, }, }; export const NearComplete: Story = { args: { - progress: 95, - description: "Almost done", + params: { + progressToken: "t3", + progress: 95, + total: 100, + message: "Almost done", + }, elapsed: "1m 30s", }, }; export const Complete: Story = { args: { - progress: 100, - description: "Done", + params: { + progressToken: "t4", + progress: 100, + total: 100, + message: "Done", + }, elapsed: "2m 15s", }, }; + +export const WithoutTotal: Story = { + args: { + params: { + progressToken: "t5", + progress: 42, + message: "Working...", + }, + }, +}; diff --git a/clients/web/src/components/elements/ProgressDisplay/ProgressDisplay.tsx b/clients/web/src/components/elements/ProgressDisplay/ProgressDisplay.tsx index 2783fd8e..7968aa3b 100644 --- a/clients/web/src/components/elements/ProgressDisplay/ProgressDisplay.tsx +++ b/clients/web/src/components/elements/ProgressDisplay/ProgressDisplay.tsx @@ -1,13 +1,20 @@ import { Group, Progress, Stack, Text } from "@mantine/core"; +import type { ProgressNotification } from "@modelcontextprotocol/sdk/types.js"; export interface ProgressDisplayProps { - progress: number; - description?: string; + params: ProgressNotification["params"]; elapsed?: string; } -function formatPercent(progress: number): string { - return `${progress}%`; +function computePercent(progress: number, total?: number): number { + if (total != null && total > 0) { + return Math.round((progress / total) * 100); + } + return progress; +} + +function formatPercent(percent: number): string { + return `${percent}%`; } const ProgressLabel = Text.withProps({ @@ -20,18 +27,16 @@ const ElapsedText = Text.withProps({ c: "dimmed", }); -export function ProgressDisplay({ - progress, - description, - elapsed, -}: ProgressDisplayProps) { +export function ProgressDisplay({ params, elapsed }: ProgressDisplayProps) { + const percent = computePercent(params.progress, params.total); + return ( - {description && {description}} - {formatPercent(progress)} + {params.message && {params.message}} + {formatPercent(percent)} - + {elapsed && {elapsed}} ); diff --git a/clients/web/src/components/elements/ServerStatusIndicator/ServerStatusIndicator.stories.tsx b/clients/web/src/components/elements/ServerStatusIndicator/ServerStatusIndicator.stories.tsx index 87385cf1..2a4ef092 100644 --- a/clients/web/src/components/elements/ServerStatusIndicator/ServerStatusIndicator.stories.tsx +++ b/clients/web/src/components/elements/ServerStatusIndicator/ServerStatusIndicator.stories.tsx @@ -28,15 +28,15 @@ export const Disconnected: Story = { }, }; -export const Failed: Story = { +export const Error: Story = { args: { - status: "failed", + status: "error", }, }; -export const FailedWithRetries: Story = { +export const ErrorWithRetries: Story = { args: { - status: "failed", + status: "error", retryCount: 3, }, }; diff --git a/clients/web/src/components/elements/ServerStatusIndicator/ServerStatusIndicator.tsx b/clients/web/src/components/elements/ServerStatusIndicator/ServerStatusIndicator.tsx index b9ace767..58f65cfc 100644 --- a/clients/web/src/components/elements/ServerStatusIndicator/ServerStatusIndicator.tsx +++ b/clients/web/src/components/elements/ServerStatusIndicator/ServerStatusIndicator.tsx @@ -1,20 +1,21 @@ import { Group, Paper, Text } from "@mantine/core"; +import type { ConnectionStatus } from "@inspector/core/mcp/types.js"; export interface ServerStatusIndicatorProps { - status: "connected" | "connecting" | "disconnected" | "failed"; + status: ConnectionStatus; latencyMs?: number; retryCount?: number; } -const statusColorVar: Record = { +const statusColorVar: Record = { connected: "var(--inspector-status-connected)", connecting: "var(--inspector-status-connecting)", disconnected: "var(--inspector-status-disconnected)", - failed: "var(--inspector-status-failed)", + error: "var(--inspector-status-error)", }; function getLabel( - status: ServerStatusIndicatorProps["status"], + status: ConnectionStatus, latencyMs?: number, retryCount?: number, ): string { @@ -25,8 +26,8 @@ function getLabel( return "Connecting..."; case "disconnected": return "Disconnected"; - case "failed": - return retryCount != null ? `Failed (${retryCount})` : "Failed"; + case "error": + return retryCount != null ? `Error (${retryCount})` : "Error"; } } diff --git a/clients/web/src/components/elements/TaskStatusBadge/TaskStatusBadge.stories.tsx b/clients/web/src/components/elements/TaskStatusBadge/TaskStatusBadge.stories.tsx index 2cdd88ef..b5c18a23 100644 --- a/clients/web/src/components/elements/TaskStatusBadge/TaskStatusBadge.stories.tsx +++ b/clients/web/src/components/elements/TaskStatusBadge/TaskStatusBadge.stories.tsx @@ -9,8 +9,8 @@ const meta: Meta = { export default meta; type Story = StoryObj; -export const Waiting: Story = { args: { status: "waiting" } }; -export const Running: Story = { args: { status: "running" } }; +export const Working: Story = { args: { status: "working" } }; +export const InputRequired: Story = { args: { status: "input_required" } }; export const Completed: Story = { args: { status: "completed" } }; export const Failed: Story = { args: { status: "failed" } }; export const Cancelled: Story = { args: { status: "cancelled" } }; diff --git a/clients/web/src/components/elements/TaskStatusBadge/TaskStatusBadge.tsx b/clients/web/src/components/elements/TaskStatusBadge/TaskStatusBadge.tsx index 49c4d786..549b1ff3 100644 --- a/clients/web/src/components/elements/TaskStatusBadge/TaskStatusBadge.tsx +++ b/clients/web/src/components/elements/TaskStatusBadge/TaskStatusBadge.tsx @@ -1,16 +1,24 @@ import { Badge, useComputedColorScheme } from "@mantine/core"; -import type { TaskStatus } from "../../groups/TaskCard/TaskCard"; +import type { TaskStatus } from "@modelcontextprotocol/sdk/types.js"; export interface TaskStatusBadgeProps { status: TaskStatus; } const statusColor: Record = { - waiting: "gray", - running: "blue", + working: "blue", + input_required: "yellow", completed: "green", failed: "red", - cancelled: "yellow", + cancelled: "gray", +}; + +const statusLabel: Record = { + working: "working", + input_required: "input required", + completed: "completed", + failed: "failed", + cancelled: "cancelled", }; export function TaskStatusBadge({ status }: TaskStatusBadgeProps) { @@ -19,7 +27,7 @@ export function TaskStatusBadge({ status }: TaskStatusBadgeProps) { return ( - {status} + {statusLabel[status]} ); } diff --git a/clients/web/src/components/elements/TransportBadge/TransportBadge.stories.tsx b/clients/web/src/components/elements/TransportBadge/TransportBadge.stories.tsx index 290c51b0..6e710864 100644 --- a/clients/web/src/components/elements/TransportBadge/TransportBadge.stories.tsx +++ b/clients/web/src/components/elements/TransportBadge/TransportBadge.stories.tsx @@ -15,8 +15,14 @@ export const Stdio: Story = { }, }; -export const Http: Story = { +export const Sse: Story = { args: { - transport: "http", + transport: "sse", + }, +}; + +export const StreamableHttp: Story = { + args: { + transport: "streamable-http", }, }; diff --git a/clients/web/src/components/elements/TransportBadge/TransportBadge.tsx b/clients/web/src/components/elements/TransportBadge/TransportBadge.tsx index d581f002..129d88e1 100644 --- a/clients/web/src/components/elements/TransportBadge/TransportBadge.tsx +++ b/clients/web/src/components/elements/TransportBadge/TransportBadge.tsx @@ -1,13 +1,20 @@ import { Badge } from "@mantine/core"; +import type { ServerType } from "@inspector/core/mcp/types.js"; export interface TransportBadgeProps { - transport: "stdio" | "http"; + transport: ServerType; } +const transportLabel: Record = { + stdio: "STDIO", + sse: "SSE", + "streamable-http": "HTTP", +}; + export function TransportBadge({ transport }: TransportBadgeProps) { return ( - {transport.toUpperCase()} + {transportLabel[transport]} ); } diff --git a/clients/web/src/components/groups/HistoryEntry/HistoryEntry.tsx b/clients/web/src/components/groups/HistoryEntry/HistoryEntry.tsx index 4d38d82b..76246ebf 100644 --- a/clients/web/src/components/groups/HistoryEntry/HistoryEntry.tsx +++ b/clients/web/src/components/groups/HistoryEntry/HistoryEntry.tsx @@ -145,8 +145,10 @@ export function HistoryEntry({ Parameters: @@ -156,8 +158,10 @@ export function HistoryEntry({ Response: diff --git a/clients/web/src/components/groups/LogStreamPanel/LogStreamPanel.stories.tsx b/clients/web/src/components/groups/LogStreamPanel/LogStreamPanel.stories.tsx index 602d79ee..c10f150e 100644 --- a/clients/web/src/components/groups/LogStreamPanel/LogStreamPanel.stories.tsx +++ b/clients/web/src/components/groups/LogStreamPanel/LogStreamPanel.stories.tsx @@ -1,7 +1,8 @@ import type { Meta, StoryObj } from "@storybook/react-vite"; +import type { LoggingLevel } from "@modelcontextprotocol/sdk/types.js"; import { fn } from "storybook/test"; import { LogStreamPanel } from "./LogStreamPanel"; -import type { LogEntryProps, LogLevel } from "../../elements/LogEntry/LogEntry"; +import type { LogEntryData } from "../../elements/LogEntry/LogEntry"; const meta: Meta = { title: "Groups/LogStreamPanel", @@ -35,40 +36,38 @@ export const Empty: Story = { }, }; -const logMessages: { level: LogLevel; message: string; logger?: string }[] = [ - { level: "info", message: "Server started on port 3000" }, - { level: "debug", message: "Loading configuration", logger: "config" }, +const logMessages: { + level: LoggingLevel; + data: string; + logger?: string; +}[] = [ + { level: "info", data: "Server started on port 3000" }, + { level: "debug", data: "Loading configuration", logger: "config" }, { level: "warning", - message: "Deprecated API endpoint called", + data: "Deprecated API endpoint called", logger: "http", }, - { level: "error", message: "Failed to read resource", logger: "resources" }, + { level: "error", data: "Failed to read resource", logger: "resources" }, { level: "info", - message: "Tool execution completed (245ms)", + data: "Tool execution completed (245ms)", logger: "tools", }, { level: "critical", - message: "Database connection pool exhausted", + data: "Database connection pool exhausted", logger: "db", }, - { level: "alert", message: "Memory usage exceeds 95%", logger: "system" }, - { level: "emergency", message: "System unresponsive", logger: "system" }, + { level: "alert", data: "Memory usage exceeds 95%", logger: "system" }, + { level: "emergency", data: "System unresponsive", logger: "system" }, ]; -function pad(n: number): string { - return n.toString().padStart(2, "0"); -} - -const entries: LogEntryProps[] = Array.from({ length: 30 }, (_, i) => { +const entries: LogEntryData[] = Array.from({ length: 30 }, (_, i) => { const src = logMessages[i % logMessages.length]; return { - timestamp: `2026-03-17T10:00:${pad(i + 1)}Z`, - level: src.level, - message: src.message, - logger: src.logger, + receivedAt: new Date(`2026-03-17T10:00:${String(i + 1).padStart(2, "0")}Z`), + params: { level: src.level, data: src.data, logger: src.logger }, }; }); diff --git a/clients/web/src/components/groups/LogStreamPanel/LogStreamPanel.tsx b/clients/web/src/components/groups/LogStreamPanel/LogStreamPanel.tsx index 979355db..d74aba20 100644 --- a/clients/web/src/components/groups/LogStreamPanel/LogStreamPanel.tsx +++ b/clients/web/src/components/groups/LogStreamPanel/LogStreamPanel.tsx @@ -10,10 +10,10 @@ import { Title, } from "@mantine/core"; import { LogEntry } from "../../elements/LogEntry/LogEntry"; -import type { LogEntryProps } from "../../elements/LogEntry/LogEntry"; +import type { LogEntryData } from "../../elements/LogEntry/LogEntry"; export interface LogStreamPanelProps { - entries: LogEntryProps[]; + entries: LogEntryData[]; filterText: string; visibleLevels: Record; autoScroll: boolean; @@ -41,16 +41,22 @@ const EmptyCenter = Stack.withProps({ justify: "center", }); +function formatData(data: unknown): string { + if (data === undefined || data === null) return ""; + if (typeof data === "string") return data; + return JSON.stringify(data); +} + function matchesFilters( - entry: LogEntryProps, + entry: LogEntryData, filterText: string, visibleLevels: Record, ): boolean { - if (!visibleLevels[entry.level]) return false; + if (!visibleLevels[entry.params.level]) return false; if (filterText) { const term = filterText.toLowerCase(); const searchable = - `${entry.message} ${entry.logger ?? ""} ${entry.level}`.toLowerCase(); + `${formatData(entry.params.data)} ${entry.params.logger ?? ""} ${entry.params.level}`.toLowerCase(); if (!searchable.includes(term)) return false; } return true; @@ -90,7 +96,7 @@ export function LogStreamPanel({ {filteredEntries.map((entry, index) => ( - + ))} diff --git a/clients/web/src/components/groups/PromptMessagesDisplay/PromptMessagesDisplay.stories.tsx b/clients/web/src/components/groups/PromptMessagesDisplay/PromptMessagesDisplay.stories.tsx index 6aed022a..78579649 100644 --- a/clients/web/src/components/groups/PromptMessagesDisplay/PromptMessagesDisplay.stories.tsx +++ b/clients/web/src/components/groups/PromptMessagesDisplay/PromptMessagesDisplay.stories.tsx @@ -16,7 +16,10 @@ export const SingleMessage: Story = { messages: [ { role: "user", - content: "What is the capital of France?", + content: { + type: "text", + text: "What is the capital of France?", + }, }, ], }, @@ -28,12 +31,17 @@ export const Conversation: Story = { messages: [ { role: "user", - content: "What is the capital of France?", + content: { + type: "text", + text: "What is the capital of France?", + }, }, { role: "assistant", - content: - "The capital of France is Paris. It is the largest city in France and serves as the country's political, economic, and cultural center.", + content: { + type: "text", + text: "The capital of France is Paris. It is the largest city in France and serves as the country's political, economic, and cultural center.", + }, }, ], }, @@ -44,11 +52,17 @@ export const WithImage: Story = { messages: [ { role: "user", - content: "Here is a photo for you to analyze.", - imageContent: { - data: "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==", - mimeType: "image/png", - }, + content: [ + { + type: "text", + text: "Here is a photo for you to analyze.", + }, + { + type: "image", + data: "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==", + mimeType: "image/png", + }, + ], }, ], }, @@ -60,25 +74,38 @@ export const LongConversation: Story = { messages: [ { role: "user", - content: "Can you help me write a haiku about programming?", + content: { + type: "text", + text: "Can you help me write a haiku about programming?", + }, }, { role: "assistant", - content: - "Sure! Here is a haiku about programming:\n\nSilent keys tap fast\nLogic flows through glowing screens\nBugs hide in the code", + content: { + type: "text", + text: "Sure! Here is a haiku about programming:\n\nSilent keys tap fast\nLogic flows through glowing screens\nBugs hide in the code", + }, }, { role: "user", - content: "That is great! Can you write another one about debugging?", + content: { + type: "text", + text: "That is great! Can you write another one about debugging?", + }, }, { role: "assistant", - content: - "Here is a debugging haiku:\n\nStack trace lines scroll down\nOne missing semicolon\nHours lost to a typo", + content: { + type: "text", + text: "Here is a debugging haiku:\n\nStack trace lines scroll down\nOne missing semicolon\nHours lost to a typo", + }, }, { role: "user", - content: "I love it! One more about open source?", + content: { + type: "text", + text: "I love it! One more about open source?", + }, }, ], }, diff --git a/clients/web/src/components/groups/PromptMessagesDisplay/PromptMessagesDisplay.tsx b/clients/web/src/components/groups/PromptMessagesDisplay/PromptMessagesDisplay.tsx index 692f9739..ecb3bbf2 100644 --- a/clients/web/src/components/groups/PromptMessagesDisplay/PromptMessagesDisplay.tsx +++ b/clients/web/src/components/groups/PromptMessagesDisplay/PromptMessagesDisplay.tsx @@ -1,20 +1,17 @@ import { Button, Group, Stack, Text, Title } from "@mantine/core"; +import type { + PromptMessage, + SamplingMessage, +} from "@modelcontextprotocol/sdk/types.js"; import { MessageBubble } from "../../elements/MessageBubble/MessageBubble"; -export interface PromptMessage { - role: string; - content: string; - imageContent?: { data: string; mimeType: string }; - audioContent?: { data: string; mimeType: string }; -} - const CopyAllButton = Button.withProps({ variant: "light", size: "sm", }); export interface PromptMessagesDisplayProps { - messages: PromptMessage[]; + messages: (PromptMessage | SamplingMessage)[]; onCopyAll?: () => void; } @@ -34,14 +31,7 @@ export function PromptMessagesDisplay({ No messages to display ) : ( messages.map((message, index) => ( - + )) )} diff --git a/clients/web/src/components/groups/ResourcePreviewPanel/ResourcePreviewPanel.tsx b/clients/web/src/components/groups/ResourcePreviewPanel/ResourcePreviewPanel.tsx index 1ba0ef28..9e1cf873 100644 --- a/clients/web/src/components/groups/ResourcePreviewPanel/ResourcePreviewPanel.tsx +++ b/clients/web/src/components/groups/ResourcePreviewPanel/ResourcePreviewPanel.tsx @@ -16,16 +16,13 @@ export interface ResourcePreviewPanelProps { onUnsubscribe: () => void; } -function priorityLabel(priority: number): string { - if (priority >= 0.7) return "priority: high"; - if (priority >= 0.4) return "priority: medium"; - return "priority: low"; -} +import type { ContentBlock } from "@modelcontextprotocol/sdk/types.js"; -function resolveContentType(mimeType: string): "json" | "image" | "text" { - if (mimeType === "application/json") return "json"; - if (mimeType.startsWith("image/")) return "image"; - return "text"; +function toContentBlock(content: string, mimeType: string): ContentBlock { + if (mimeType.startsWith("image/")) { + return { type: "image", data: content, mimeType }; + } + return { type: "text", text: content }; } function formatLastUpdated(lastUpdated: string): string { @@ -97,12 +94,7 @@ export function ResourcePreviewPanel({ - + {lastUpdated ? ( {formatLastUpdated(lastUpdated)} @@ -114,13 +106,15 @@ export function ResourcePreviewPanel({ {annotations?.audience && ( - + )} {annotations?.priority !== undefined && ( - + )} diff --git a/clients/web/src/components/groups/ResourceTemplatePanel/ResourceTemplatePanel.tsx b/clients/web/src/components/groups/ResourceTemplatePanel/ResourceTemplatePanel.tsx index 31d50f42..febbc60b 100644 --- a/clients/web/src/components/groups/ResourceTemplatePanel/ResourceTemplatePanel.tsx +++ b/clients/web/src/components/groups/ResourceTemplatePanel/ResourceTemplatePanel.tsx @@ -40,12 +40,6 @@ function previewUri( ); } -function priorityLabel(priority: number): string { - if (priority >= 0.7) return "priority: high"; - if (priority >= 0.4) return "priority: medium"; - return "priority: low"; -} - const HeaderRow = Group.withProps({ justify: "space-between", wrap: "nowrap", @@ -128,13 +122,15 @@ export function ResourceTemplatePanel({ {annotations?.audience && ( - + )} {annotations?.priority !== undefined && ( - + )} ); diff --git a/clients/web/src/components/groups/HistoryListPanel/HistoryListPanel.tsx b/clients/web/src/components/groups/HistoryListPanel/HistoryListPanel.tsx index e0094a9a..0940a35d 100644 --- a/clients/web/src/components/groups/HistoryListPanel/HistoryListPanel.tsx +++ b/clients/web/src/components/groups/HistoryListPanel/HistoryListPanel.tsx @@ -22,7 +22,7 @@ export interface HistoryRequestsPanelProps { } const ToolbarButton = Button.withProps({ - variant: "light", + variant: "subtle", size: "sm", }); @@ -96,7 +96,6 @@ export function HistoryListPanel({ Requests Export JSON - Clear All {hasResults && ( 0 && ( <> - - {formatHistoryTitle(filteredEntries.length)} - + + + {formatHistoryTitle(filteredEntries.length)} + + Clear + {filteredEntries.map((entry) => ( {LOG_LEVELS.map((level) => { const style = LEVEL_COLORS[level]; - const active = !!visibleLevels[level]; + const active = visibleLevels[level]; return ( - setOpenSections(allExpanded ? [] : [...allSections])} - /> setSearchText(e.currentTarget.value)} /> + setOpenSections(allExpanded ? [] : [...allSections])} + /> diff --git a/clients/web/src/components/groups/TaskControls/TaskControls.stories.tsx b/clients/web/src/components/groups/TaskControls/TaskControls.stories.tsx index bb14e11b..83aae64b 100644 --- a/clients/web/src/components/groups/TaskControls/TaskControls.stories.tsx +++ b/clients/web/src/components/groups/TaskControls/TaskControls.stories.tsx @@ -10,7 +10,6 @@ const meta: Meta = { onSearchChange: fn(), onStatusFilterChange: fn(), onRefresh: fn(), - onClearHistory: fn(), }, }; diff --git a/clients/web/src/components/groups/TaskControls/TaskControls.tsx b/clients/web/src/components/groups/TaskControls/TaskControls.tsx index c6939fa2..9bff8aa4 100644 --- a/clients/web/src/components/groups/TaskControls/TaskControls.tsx +++ b/clients/web/src/components/groups/TaskControls/TaskControls.tsx @@ -10,7 +10,7 @@ const STATUS_OPTIONS: TaskStatus[] = [ ]; const ToolbarButton = Button.withProps({ - variant: "light", + variant: "subtle", size: "sm", }); @@ -20,7 +20,6 @@ export interface TaskControlsProps { onSearchChange: (text: string) => void; onStatusFilterChange: (status: string) => void; onRefresh: () => void; - onClearHistory: () => void; } export function TaskControls({ @@ -29,12 +28,13 @@ export function TaskControls({ onSearchChange, onStatusFilterChange, onRefresh, - onClearHistory, }: TaskControlsProps) { return ( - Tasks - + + Tasks + Refresh + Search onStatusFilterChange(value ?? "")} clearable /> - - - Refresh - Clear History - ); } diff --git a/clients/web/src/components/groups/TaskListPanel/TaskListPanel.stories.tsx b/clients/web/src/components/groups/TaskListPanel/TaskListPanel.stories.tsx index 6ed15198..b516b9d2 100644 --- a/clients/web/src/components/groups/TaskListPanel/TaskListPanel.stories.tsx +++ b/clients/web/src/components/groups/TaskListPanel/TaskListPanel.stories.tsx @@ -61,6 +61,7 @@ const meta: Meta = { args: { tasks: sampleTasks, searchText: "", + onClearCompleted: fn(), }, }; diff --git a/clients/web/src/components/groups/TaskListPanel/TaskListPanel.tsx b/clients/web/src/components/groups/TaskListPanel/TaskListPanel.tsx index 15e6d290..57dcad0b 100644 --- a/clients/web/src/components/groups/TaskListPanel/TaskListPanel.tsx +++ b/clients/web/src/components/groups/TaskListPanel/TaskListPanel.tsx @@ -1,5 +1,13 @@ import { useMemo, useState } from "react"; -import { Group, Paper, ScrollArea, Stack, Text, Title } from "@mantine/core"; +import { + Button, + Group, + Paper, + ScrollArea, + Stack, + Text, + Title, +} from "@mantine/core"; import { TaskCard } from "../TaskCard/TaskCard"; import { ListToggle } from "../../elements/ListToggle/ListToggle"; import type { TaskCardProps } from "../TaskCard/TaskCard"; @@ -8,6 +16,7 @@ export interface TaskListPanelProps { tasks: TaskCardProps[]; searchText: string; statusFilter?: string; + onClearCompleted: () => void; } const PanelContainer = Paper.withProps({ @@ -17,6 +26,11 @@ const PanelContainer = Paper.withProps({ variant: "panel", }); +const ClearHistoryButton = Button.withProps({ + variant: "subtle", + size: "sm", +}); + const EmptyState = Text.withProps({ c: "dimmed", ta: "center", @@ -58,6 +72,7 @@ export function TaskListPanel({ tasks, searchText, statusFilter, + onClearCompleted, }: TaskListPanelProps) { const [compact, setCompact] = useState(false); @@ -110,9 +125,14 @@ export function TaskListPanel({ {completedTasks.length > 0 && ( <> - - {formatCompletedTitle(completedTasks.length)} - + + + {formatCompletedTitle(completedTasks.length)} + + + Clear + + {completedTasks.map((task) => ( - + {selectedPrompt ? ( <> @@ -108,7 +109,7 @@ export function PromptsScreen({ Select a prompt to view details )} - + ); diff --git a/clients/web/src/components/screens/TasksScreen/TasksScreen.stories.tsx b/clients/web/src/components/screens/TasksScreen/TasksScreen.stories.tsx index 189f9667..40085855 100644 --- a/clients/web/src/components/screens/TasksScreen/TasksScreen.stories.tsx +++ b/clients/web/src/components/screens/TasksScreen/TasksScreen.stories.tsx @@ -9,7 +9,7 @@ const meta: Meta = { parameters: { layout: "fullscreen" }, args: { onRefresh: fn(), - onClearHistory: fn(), + onClearCompleted: fn(), }, }; diff --git a/clients/web/src/components/screens/TasksScreen/TasksScreen.tsx b/clients/web/src/components/screens/TasksScreen/TasksScreen.tsx index 700365b5..fd7b4417 100644 --- a/clients/web/src/components/screens/TasksScreen/TasksScreen.tsx +++ b/clients/web/src/components/screens/TasksScreen/TasksScreen.tsx @@ -7,7 +7,7 @@ import type { TaskCardProps } from "../../groups/TaskCard/TaskCard"; export interface TasksScreenProps { tasks: TaskCardProps[]; onRefresh: () => void; - onClearHistory: () => void; + onClearCompleted: () => void; } const ScreenLayout = Flex.withProps({ @@ -30,7 +30,7 @@ const SidebarCard = Card.withProps({ export function TasksScreen({ tasks, onRefresh, - onClearHistory, + onClearCompleted, }: TasksScreenProps) { const [searchText, setSearchText] = useState(""); const [statusFilter, setStatusFilter] = useState(); @@ -47,7 +47,6 @@ export function TasksScreen({ setStatusFilter(value || undefined) } onRefresh={onRefresh} - onClearHistory={onClearHistory} /> @@ -55,6 +54,7 @@ export function TasksScreen({ tasks={tasks} searchText={searchText} statusFilter={statusFilter} + onClearCompleted={onClearCompleted} /> ); diff --git a/clients/web/src/components/views/ConnectedView/ConnectedView.stories.tsx b/clients/web/src/components/views/ConnectedView/ConnectedView.stories.tsx index c18dcb76..d5f6dcde 100644 --- a/clients/web/src/components/views/ConnectedView/ConnectedView.stories.tsx +++ b/clients/web/src/components/views/ConnectedView/ConnectedView.stories.tsx @@ -762,7 +762,7 @@ export const TasksActive: Story = { }, ]} onRefresh={fn()} - onClearHistory={fn()} + onClearCompleted={fn()} /> ), }, diff --git a/clients/web/src/main.tsx b/clients/web/src/main.tsx index 29a1d867..63c99077 100644 --- a/clients/web/src/main.tsx +++ b/clients/web/src/main.tsx @@ -1,6 +1,6 @@ import { StrictMode } from "react"; import { createRoot } from "react-dom/client"; -import { MantineProvider } from "@mantine/core"; +import { MantineProvider, type CSSVariablesResolver } from "@mantine/core"; import { Notifications } from "@mantine/notifications"; import "@mantine/core/styles.css"; import "@mantine/notifications/styles.css"; @@ -8,9 +8,21 @@ import "./App.css"; import { theme } from "./theme/theme"; import App from "./App"; +const resolver: CSSVariablesResolver = () => ({ + variables: {}, + light: {}, + dark: { + "--mantine-color-body": "var(--mantine-color-blue-9)", + }, +}); + createRoot(document.getElementById("root")!).render( - + diff --git a/clients/web/src/theme/Input.ts b/clients/web/src/theme/Input.ts new file mode 100644 index 00000000..9b8e5f04 --- /dev/null +++ b/clients/web/src/theme/Input.ts @@ -0,0 +1,9 @@ +import { Input } from "@mantine/core"; + +export const ThemeInput = Input.extend({ + styles: () => ({ + input: { + backgroundColor: "var(--inspector-input-background)", + }, + }), +}); diff --git a/clients/web/src/theme/index.ts b/clients/web/src/theme/index.ts index 91835be9..42b0cd1e 100644 --- a/clients/web/src/theme/index.ts +++ b/clients/web/src/theme/index.ts @@ -6,6 +6,7 @@ export { ThemeButton } from "./Button"; export { ThemeCard } from "./Card"; export { ThemeCode } from "./Code"; export { ThemeFlex } from "./Flex"; +export { ThemeInput } from "./Input"; export { ThemePaper } from "./Paper"; export { ThemeSelect } from "./Select"; export { ThemeSwitch } from "./Switch"; diff --git a/clients/web/src/theme/theme.ts b/clients/web/src/theme/theme.ts index d79a8856..bacebf3c 100644 --- a/clients/web/src/theme/theme.ts +++ b/clients/web/src/theme/theme.ts @@ -1,4 +1,4 @@ -import { createTheme } from "@mantine/core"; +import { createTheme, type MantineColorsTuple } from "@mantine/core"; import { ThemeActionIcon, ThemeAlert, @@ -8,6 +8,7 @@ import { ThemeCard, ThemeCode, ThemeFlex, + ThemeInput, ThemePaper, ThemeSelect, ThemeSwitch, @@ -15,10 +16,26 @@ import { ThemeUnstyledButton, } from "./index"; +const inspector: MantineColorsTuple = [ + "#f0f3fa", + "#dee3ee", + "#b9c5de", + "#91a5cf", + "#7089c3", + "#5b78bb", + "#4f70b9", + "#405fa3", + "#375493", + "#121e36", +]; + export const theme = createTheme({ + colors: { + inspector, + }, /* ── Color ──────────────────────────────────────────────── */ - primaryColor: "dark", - primaryShade: { light: 7, dark: 4 }, + primaryColor: "inspector", + primaryShade: { light: 7, dark: 8 }, autoContrast: true, /* ── Shape ──────────────────────────────────────────────── */ @@ -43,6 +60,7 @@ export const theme = createTheme({ Card: ThemeCard, Code: ThemeCode, Flex: ThemeFlex, + Input: ThemeInput, Paper: ThemePaper, Select: ThemeSelect, Switch: ThemeSwitch, From 3e2e44ccfcb572faaf43c3cd262c8616d87a3745 Mon Sep 17 00:00:00 2001 From: cliffhall Date: Sat, 18 Apr 2026 11:48:21 -0400 Subject: [PATCH 4/7] Address PR #1211 code review feedback 1. ServerCard: import ConnectionStatus and ServerType from core instead of inline union types 2. ResourcePreviewPanel: move ContentBlock import to top of file 3. ContentViewer: remove dead annotation mimeType check in isJsonText 4. ProgressDisplay: use Pick to exclude progressToken from params type; remove fake tokens from callers and stories 5. AnnotationBadge: restore human-readable priority labels (high/medium/low) and remove unused --inspector-annotation-* CSS vars from App.css 6. MessageBubble: add fallback rendering for resource, resource_link, tool_use, and tool_result content block types 7. TaskControls: import TaskStatus directly from SDK instead of via TaskCard Co-Authored-By: Claude Opus 4.6 (1M context) --- clients/web/src/App.css | 9 --------- .../elements/AnnotationBadge/AnnotationBadge.tsx | 8 ++++++-- .../elements/ContentViewer/ContentViewer.tsx | 10 ---------- .../elements/MessageBubble/MessageBubble.tsx | 12 ++++++++++++ .../ProgressDisplay/ProgressDisplay.stories.tsx | 6 +----- .../elements/ProgressDisplay/ProgressDisplay.tsx | 5 ++++- .../ResourcePreviewPanel/ResourcePreviewPanel.tsx | 3 +-- .../src/components/groups/ServerCard/ServerCard.tsx | 8 ++++++-- .../web/src/components/groups/TaskCard/TaskCard.tsx | 1 - .../components/groups/TaskControls/TaskControls.tsx | 2 +- .../groups/ToolDetailPanel/ToolDetailPanel.tsx | 1 - 11 files changed, 31 insertions(+), 34 deletions(-) diff --git a/clients/web/src/App.css b/clients/web/src/App.css index 5a0a75f2..47b315bd 100644 --- a/clients/web/src/App.css +++ b/clients/web/src/App.css @@ -49,15 +49,6 @@ --inspector-log-alert: var(--mantine-color-red-9); --inspector-log-emergency: var(--mantine-color-red-9); - /* ── Annotations ───────────────────────────────────────── */ - --inspector-annotation-destructive: var(--mantine-color-red-6); - --inspector-annotation-long-run: var(--mantine-color-yellow-6); - --inspector-annotation-read-only: var(--mantine-color-dark-6); - --inspector-annotation-audience: var(--mantine-color-gray-6); - --inspector-annotation-priority-high: var(--mantine-color-orange-6); - --inspector-annotation-priority-medium: var(--mantine-color-yellow-6); - --inspector-annotation-priority-low: var(--mantine-color-green-6); - /* ── Task status ───────────────────────────────────────── */ --inspector-task-waiting: var(--mantine-color-gray-5); --inspector-task-running: var(--mantine-color-blue-6); diff --git a/clients/web/src/components/elements/AnnotationBadge/AnnotationBadge.tsx b/clients/web/src/components/elements/AnnotationBadge/AnnotationBadge.tsx index babee2f4..9f1f08f9 100644 --- a/clients/web/src/components/elements/AnnotationBadge/AnnotationBadge.tsx +++ b/clients/web/src/components/elements/AnnotationBadge/AnnotationBadge.tsx @@ -32,8 +32,12 @@ function formatLabel( switch (facet) { case "audience": return `audience: ${(value as Role[]).join(", ")}`; - case "priority": - return `priority: ${value}`; + case "priority": { + const n = value as number; + if (n >= 0.7) return "priority: high"; + if (n >= 0.4) return "priority: medium"; + return "priority: low"; + } case "readOnlyHint": return "read-only"; case "destructiveHint": diff --git a/clients/web/src/components/elements/ContentViewer/ContentViewer.tsx b/clients/web/src/components/elements/ContentViewer/ContentViewer.tsx index 74d32716..a5710e32 100644 --- a/clients/web/src/components/elements/ContentViewer/ContentViewer.tsx +++ b/clients/web/src/components/elements/ContentViewer/ContentViewer.tsx @@ -17,16 +17,6 @@ function formatJson(content: string): string { function isJsonText(block: ContentBlock): boolean { if (block.type !== "text") return false; - if ( - "annotations" in block && - block.annotations && - "mimeType" in block.annotations - ) { - return ( - (block.annotations as Record).mimeType === - "application/json" - ); - } const trimmed = block.text.trimStart(); return trimmed.startsWith("{") || trimmed.startsWith("["); } diff --git a/clients/web/src/components/elements/MessageBubble/MessageBubble.tsx b/clients/web/src/components/elements/MessageBubble/MessageBubble.tsx index d1ba15ec..68465ed6 100644 --- a/clients/web/src/components/elements/MessageBubble/MessageBubble.tsx +++ b/clients/web/src/components/elements/MessageBubble/MessageBubble.tsx @@ -51,6 +51,18 @@ function extractContent( audioUri = buildDataUri(block.mimeType, block.data); audioMime = block.mimeType; break; + case "resource": + text += + "text" in block.resource + ? block.resource.text + : `[resource: ${block.resource.uri}]`; + break; + case "resource_link": + text += `[resource: ${block.uri}]`; + break; + default: + text += `[${block.type}]`; + break; } } diff --git a/clients/web/src/components/elements/ProgressDisplay/ProgressDisplay.stories.tsx b/clients/web/src/components/elements/ProgressDisplay/ProgressDisplay.stories.tsx index 23bab30c..914b866b 100644 --- a/clients/web/src/components/elements/ProgressDisplay/ProgressDisplay.stories.tsx +++ b/clients/web/src/components/elements/ProgressDisplay/ProgressDisplay.stories.tsx @@ -11,14 +11,13 @@ type Story = StoryObj; export const ZeroPercent: Story = { args: { - params: { progressToken: "t1", progress: 0, total: 100 }, + params: { progress: 0, total: 100 }, }, }; export const HalfComplete: Story = { args: { params: { - progressToken: "t2", progress: 50, total: 100, message: "Processing...", @@ -29,7 +28,6 @@ export const HalfComplete: Story = { export const NearComplete: Story = { args: { params: { - progressToken: "t3", progress: 95, total: 100, message: "Almost done", @@ -41,7 +39,6 @@ export const NearComplete: Story = { export const Complete: Story = { args: { params: { - progressToken: "t4", progress: 100, total: 100, message: "Done", @@ -53,7 +50,6 @@ export const Complete: Story = { export const WithoutTotal: Story = { args: { params: { - progressToken: "t5", progress: 42, message: "Working...", }, diff --git a/clients/web/src/components/elements/ProgressDisplay/ProgressDisplay.tsx b/clients/web/src/components/elements/ProgressDisplay/ProgressDisplay.tsx index 7968aa3b..1e03791d 100644 --- a/clients/web/src/components/elements/ProgressDisplay/ProgressDisplay.tsx +++ b/clients/web/src/components/elements/ProgressDisplay/ProgressDisplay.tsx @@ -2,7 +2,10 @@ import { Group, Progress, Stack, Text } from "@mantine/core"; import type { ProgressNotification } from "@modelcontextprotocol/sdk/types.js"; export interface ProgressDisplayProps { - params: ProgressNotification["params"]; + params: Pick< + ProgressNotification["params"], + "progress" | "total" | "message" + >; elapsed?: string; } diff --git a/clients/web/src/components/groups/ResourcePreviewPanel/ResourcePreviewPanel.tsx b/clients/web/src/components/groups/ResourcePreviewPanel/ResourcePreviewPanel.tsx index 9e1cf873..7ee3c638 100644 --- a/clients/web/src/components/groups/ResourcePreviewPanel/ResourcePreviewPanel.tsx +++ b/clients/web/src/components/groups/ResourcePreviewPanel/ResourcePreviewPanel.tsx @@ -1,4 +1,5 @@ import { Button, Flex, Group, Stack, Text, Title } from "@mantine/core"; +import type { ContentBlock } from "@modelcontextprotocol/sdk/types.js"; import { AnnotationBadge } from "../../elements/AnnotationBadge/AnnotationBadge"; import { ContentViewer } from "../../elements/ContentViewer/ContentViewer"; import { CopyButton } from "../../elements/CopyButton/CopyButton"; @@ -16,8 +17,6 @@ export interface ResourcePreviewPanelProps { onUnsubscribe: () => void; } -import type { ContentBlock } from "@modelcontextprotocol/sdk/types.js"; - function toContentBlock(content: string, mimeType: string): ContentBlock { if (mimeType.startsWith("image/")) { return { type: "image", data: content, mimeType }; diff --git a/clients/web/src/components/groups/ServerCard/ServerCard.tsx b/clients/web/src/components/groups/ServerCard/ServerCard.tsx index dc4f8c42..20db7895 100644 --- a/clients/web/src/components/groups/ServerCard/ServerCard.tsx +++ b/clients/web/src/components/groups/ServerCard/ServerCard.tsx @@ -1,5 +1,9 @@ import { useEffect } from "react"; import { Badge, Button, Card, Group, Menu, Stack, Text } from "@mantine/core"; +import type { + ConnectionStatus, + ServerType, +} from "@inspector/core/mcp/types.js"; import { ServerStatusIndicator } from "../../elements/ServerStatusIndicator/ServerStatusIndicator"; import { TransportBadge } from "../../elements/TransportBadge/TransportBadge"; import { ConnectionToggle } from "../../elements/ConnectionToggle/ConnectionToggle"; @@ -9,10 +13,10 @@ import { InlineError } from "../../elements/InlineError/InlineError"; export interface ServerCardProps { name: string; version?: string; - transport: "stdio" | "sse" | "streamable-http"; + transport: ServerType; connectionMode: string; command: string; - status: "connected" | "connecting" | "disconnected" | "error"; + status: ConnectionStatus; retryCount?: number; error?: { message: string; details?: string }; canTestClientFeatures: boolean; diff --git a/clients/web/src/components/groups/TaskCard/TaskCard.tsx b/clients/web/src/components/groups/TaskCard/TaskCard.tsx index 736818a6..1ff14d07 100644 --- a/clients/web/src/components/groups/TaskCard/TaskCard.tsx +++ b/clients/web/src/components/groups/TaskCard/TaskCard.tsx @@ -197,7 +197,6 @@ export function TaskCard(props: TaskCardProps) { {progress !== undefined && isActive && ( Date: Sat, 18 Apr 2026 12:12:41 -0400 Subject: [PATCH 5/7] Fix HistoryControls Select clear button requiring double click Same fix as TaskControls: pass null instead of undefined to Mantine Select value prop so the clear button disappears on first click. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../src/components/groups/HistoryControls/HistoryControls.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/clients/web/src/components/groups/HistoryControls/HistoryControls.tsx b/clients/web/src/components/groups/HistoryControls/HistoryControls.tsx index dacb874b..086e6526 100644 --- a/clients/web/src/components/groups/HistoryControls/HistoryControls.tsx +++ b/clients/web/src/components/groups/HistoryControls/HistoryControls.tsx @@ -37,7 +37,7 @@ export function HistoryControls({