From e6de97dc203b114c6b4af21afef12033dca4bd76 Mon Sep 17 00:00:00 2001 From: cliffhall Date: Tue, 21 Apr 2026 12:05:55 -0400 Subject: [PATCH] Phase 2: Refactor Roots/Schema/Experimental cluster to MCP SDK types MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit RootsTable: Replace local RootEntry with SDK Root type; consolidate newRootName/newRootPath into newRootDraft object; rename Path→URI label. SchemaForm: Replace local JsonSchema with shared JsonSchemaType from utils/jsonUtils; update consumers (ElicitationFormPanel, InlineElicitationRequest) to import from shared location. ExperimentalFeaturesPanel: Replace array-based ExperimentalCapability with ServerCapabilities['experimental'] freeform record; replace responseJson string with JSONRPCResponse | JSONRPCErrorResponse; rename requestJson→requestDraft; use Date for RequestHistoryItem.timestamp; rename KeyValuePair→HeaderPair; add error badge for error responses. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../ElicitationFormPanel.tsx | 4 +- .../ExperimentalFeaturesPanel.stories.tsx | 88 ++++++++------ .../ExperimentalFeaturesPanel.tsx | 111 +++++++++++------- .../InlineElicitationRequest.tsx | 4 +- .../groups/RootsTable/RootsTable.stories.tsx | 38 +++--- .../groups/RootsTable/RootsTable.tsx | 40 ++++--- .../groups/SchemaForm/SchemaForm.tsx | 35 ++---- 7 files changed, 177 insertions(+), 143 deletions(-) diff --git a/clients/web/src/components/groups/ElicitationFormPanel/ElicitationFormPanel.tsx b/clients/web/src/components/groups/ElicitationFormPanel/ElicitationFormPanel.tsx index 2ec2bdf9..fbe6e4f4 100644 --- a/clients/web/src/components/groups/ElicitationFormPanel/ElicitationFormPanel.tsx +++ b/clients/web/src/components/groups/ElicitationFormPanel/ElicitationFormPanel.tsx @@ -1,7 +1,7 @@ import { Alert, Button, Divider, Group, Stack, Text } from "@mantine/core"; import type { ElicitRequestFormParams } from "@modelcontextprotocol/sdk/types.js"; +import type { JsonSchemaType } from "../../../utils/jsonUtils"; import { SchemaForm } from "../SchemaForm/SchemaForm"; -import type { JsonSchema } from "../SchemaForm/SchemaForm"; export interface ElicitationFormPanelProps { request: ElicitRequestFormParams; @@ -38,7 +38,7 @@ export function ElicitationFormPanel({ {formatQuoted(request.message)} diff --git a/clients/web/src/components/groups/ExperimentalFeaturesPanel/ExperimentalFeaturesPanel.stories.tsx b/clients/web/src/components/groups/ExperimentalFeaturesPanel/ExperimentalFeaturesPanel.stories.tsx index 10bc307e..e2f04be8 100644 --- a/clients/web/src/components/groups/ExperimentalFeaturesPanel/ExperimentalFeaturesPanel.stories.tsx +++ b/clients/web/src/components/groups/ExperimentalFeaturesPanel/ExperimentalFeaturesPanel.stories.tsx @@ -1,7 +1,16 @@ import type { Meta, StoryObj } from "@storybook/react-vite"; import { fn } from "storybook/test"; +import type { + ClientExperimentalToggle, + RequestHistoryItem, +} from "./ExperimentalFeaturesPanel"; import { ExperimentalFeaturesPanel } from "./ExperimentalFeaturesPanel"; +const defaultClientToggles: ClientExperimentalToggle[] = [ + { name: "experimental/customSampling", enabled: false }, + { name: "experimental/batchRequests", enabled: true }, +]; + const meta: Meta = { title: "Groups/ExperimentalFeaturesPanel", component: ExperimentalFeaturesPanel, @@ -14,13 +23,11 @@ const meta: Meta = { onHeaderChange: fn(), onCopyResponse: fn(), onTestCapability: fn(), - clientCapabilities: [ - { name: "experimental/customSampling", enabled: false }, - { name: "experimental/batchRequests", enabled: true }, - ], + clientExperimental: {}, + clientToggles: defaultClientToggles, customHeaders: [], requestHistory: [], - requestJson: JSON.stringify( + requestDraft: JSON.stringify( { jsonrpc: "2.0", id: 1, @@ -38,14 +45,12 @@ type Story = StoryObj; export const WithServerCaps: Story = { args: { - serverCapabilities: [ - { - name: "experimental/streaming", + serverExperimental: { + "experimental/streaming": { description: "Supports streaming responses for long-running operations", methods: ["experimental/stream.start", "experimental/stream.cancel"], }, - { - name: "experimental/caching", + "experimental/caching": { description: "Server-side response caching with TTL support", methods: [ "experimental/cache.get", @@ -53,37 +58,31 @@ export const WithServerCaps: Story = { "experimental/cache.clear", ], }, - ], + }, }, }; export const NoServerCaps: Story = { args: { - serverCapabilities: [], + serverExperimental: undefined, }, }; export const WithResponse: Story = { args: { - serverCapabilities: [ - { - name: "experimental/echo", + serverExperimental: { + "experimental/echo": { description: "Echoes back the request for testing", - methods: ["experimental/echo"], }, - ], - responseJson: JSON.stringify( - { - jsonrpc: "2.0", - id: 1, - result: { - echo: "Hello from experimental endpoint", - timestamp: "2026-03-17T10:30:00Z", - }, + }, + response: { + jsonrpc: "2.0" as const, + id: 1, + result: { + echo: "Hello from experimental endpoint", + timestamp: "2026-03-17T10:30:00Z", }, - null, - 2, - ), + }, customHeaders: [ { key: "X-Custom-Auth", value: "Bearer token123" }, { key: "X-Request-Id", value: "req-abc-456" }, @@ -91,33 +90,50 @@ export const WithResponse: Story = { }, }; +export const WithErrorResponse: Story = { + args: { + serverExperimental: { + "experimental/echo": { + description: "Echoes back the request for testing", + }, + }, + response: { + jsonrpc: "2.0" as const, + id: 1, + error: { + code: -32601, + message: "Method not found", + }, + }, + }, +}; + export const WithHistory: Story = { args: { - serverCapabilities: [ - { - name: "experimental/metrics", + serverExperimental: { + "experimental/metrics": { description: "Exposes server metrics", }, - ], + }, requestHistory: [ { - timestamp: "2026-03-17 10:30:15", + timestamp: new Date("2026-03-17T10:30:15Z"), method: "experimental/metrics.get", status: "success", durationMs: 42, }, { - timestamp: "2026-03-17 10:29:50", + timestamp: new Date("2026-03-17T10:29:50Z"), method: "experimental/echo", status: "success", durationMs: 15, }, { - timestamp: "2026-03-17 10:28:30", + timestamp: new Date("2026-03-17T10:28:30Z"), method: "experimental/unknown", status: "error", durationMs: 120, }, - ], + ] satisfies RequestHistoryItem[], }, }; diff --git a/clients/web/src/components/groups/ExperimentalFeaturesPanel/ExperimentalFeaturesPanel.tsx b/clients/web/src/components/groups/ExperimentalFeaturesPanel/ExperimentalFeaturesPanel.tsx index 401fea6b..e245fc41 100644 --- a/clients/web/src/components/groups/ExperimentalFeaturesPanel/ExperimentalFeaturesPanel.tsx +++ b/clients/web/src/components/groups/ExperimentalFeaturesPanel/ExperimentalFeaturesPanel.tsx @@ -1,6 +1,7 @@ import { ActionIcon, Alert, + Badge, Button, Card, Checkbox, @@ -13,36 +14,37 @@ import { Textarea, Title, } from "@mantine/core"; +import type { + ClientCapabilities, + JSONRPCErrorResponse, + JSONRPCResponse, + ServerCapabilities, +} from "@modelcontextprotocol/sdk/types.js"; -export interface ExperimentalCapability { - name: string; - description?: string; - methods?: string[]; -} - -export interface ClientExperimentalCapability { +export interface ClientExperimentalToggle { name: string; enabled: boolean; } -export interface KeyValuePair { +export interface HeaderPair { key: string; value: string; } export interface RequestHistoryItem { - timestamp: string; + timestamp: Date; method: string; status: string; durationMs: number; } export interface ExperimentalFeaturesPanelProps { - serverCapabilities: ExperimentalCapability[]; - clientCapabilities: ClientExperimentalCapability[]; - requestJson: string; - responseJson?: string; - customHeaders: KeyValuePair[]; + serverExperimental: ServerCapabilities["experimental"]; + clientExperimental: ClientCapabilities["experimental"]; + clientToggles: ClientExperimentalToggle[]; + requestDraft: string; + response?: JSONRPCResponse | JSONRPCErrorResponse; + customHeaders: HeaderPair[]; requestHistory: RequestHistoryItem[]; onToggleClientCapability: (name: string, enabled: boolean) => void; onRequestChange: (json: string) => void; @@ -79,19 +81,38 @@ const RemoveIcon = ActionIcon.withProps({ color: "red", }); -function formatMethods(methods: string[]): string { - return `Methods: ${methods.join(", ")}`; -} - function formatDuration(ms: number): string { return `${ms}ms`; } +function formatTimestamp(date: Date): string { + return date.toLocaleString(); +} + +function formatResponse( + response: JSONRPCResponse | JSONRPCErrorResponse, +): string { + return JSON.stringify(response, null, 2); +} + +function isErrorResponse( + response: JSONRPCResponse | JSONRPCErrorResponse, +): response is JSONRPCErrorResponse { + return "error" in response; +} + +function getCapabilityEntries( + experimental: ServerCapabilities["experimental"], +): [string, object][] { + if (!experimental) return []; + return Object.entries(experimental); +} + export function ExperimentalFeaturesPanel({ - serverCapabilities, - clientCapabilities, - requestJson, - responseJson, + serverExperimental, + clientToggles, + requestDraft, + response, customHeaders, requestHistory, onToggleClientCapability, @@ -103,6 +124,8 @@ export function ExperimentalFeaturesPanel({ onCopyResponse, onTestCapability, }: ExperimentalFeaturesPanelProps) { + const serverEntries = getCapabilityEntries(serverExperimental); + return ( @@ -111,19 +134,16 @@ export function ExperimentalFeaturesPanel({ Server Experimental Capabilities: - {serverCapabilities.length === 0 ? ( + {serverEntries.length === 0 ? ( No experimental capabilities ) : ( - serverCapabilities.map((cap) => ( - + serverEntries.map(([name, value]) => ( + - {cap.name} - {cap.description && {cap.description}} - {cap.methods && cap.methods.length > 0 && ( - {formatMethods(cap.methods)} - )} + {name} + {JSON.stringify(value)} - onTestCapability(cap.name)}> + onTestCapability(name)}> Test → @@ -136,13 +156,13 @@ export function ExperimentalFeaturesPanel({ Client Experimental Capabilities: - {clientCapabilities.map((clientCap) => ( + {clientToggles.map((toggle) => ( - onToggleClientCapability(clientCap.name, e.currentTarget.checked) + onToggleClientCapability(toggle.name, e.currentTarget.checked) } /> ))} @@ -182,7 +202,7 @@ export function ExperimentalFeaturesPanel({