Phase 2: Refactor Roots/Schema/Experimental cluster to MCP SDK types

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) <noreply@anthropic.com>
This commit is contained in:
cliffhall 2026-04-21 12:05:55 -04:00
commit e6de97dc20
2 changed files with 177 additions and 143 deletions

View file

@ -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<typeof ExperimentalFeaturesPanel> = {
title: "Groups/ExperimentalFeaturesPanel",
component: ExperimentalFeaturesPanel,
@ -14,13 +23,11 @@ const meta: Meta<typeof ExperimentalFeaturesPanel> = {
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<typeof ExperimentalFeaturesPanel>;
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[],
},
};

View file

@ -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 (
<Stack gap="md">
<Alert color="yellow">
@ -111,19 +134,16 @@ export function ExperimentalFeaturesPanel({
<Title order={5}>Server Experimental Capabilities:</Title>
{serverCapabilities.length === 0 ? (
{serverEntries.length === 0 ? (
<Text c="dimmed">No experimental capabilities</Text>
) : (
serverCapabilities.map((cap) => (
<CapCard key={cap.name}>
serverEntries.map(([name, value]) => (
<CapCard key={name}>
<Stack gap="xs">
<Text fw={600}>{cap.name}</Text>
{cap.description && <HintText>{cap.description}</HintText>}
{cap.methods && cap.methods.length > 0 && (
<MetaText>{formatMethods(cap.methods)}</MetaText>
)}
<Text fw={600}>{name}</Text>
<MetaText>{JSON.stringify(value)}</MetaText>
<Group>
<CompactButton onClick={() => onTestCapability(cap.name)}>
<CompactButton onClick={() => onTestCapability(name)}>
Test →
</CompactButton>
</Group>
@ -136,13 +156,13 @@ export function ExperimentalFeaturesPanel({
<Title order={5}>Client Experimental Capabilities:</Title>
{clientCapabilities.map((clientCap) => (
{clientToggles.map((toggle) => (
<Checkbox
key={clientCap.name}
label={clientCap.name}
checked={clientCap.enabled}
key={toggle.name}
label={toggle.name}
checked={toggle.enabled}
onChange={(e) =>
onToggleClientCapability(clientCap.name, e.currentTarget.checked)
onToggleClientCapability(toggle.name, e.currentTarget.checked)
}
/>
))}
@ -182,7 +202,7 @@ export function ExperimentalFeaturesPanel({
<Textarea
label="Request"
ff="monospace"
value={requestJson}
value={requestDraft}
onChange={(e) => onRequestChange(e.currentTarget.value)}
autosize
minRows={6}
@ -190,12 +210,21 @@ export function ExperimentalFeaturesPanel({
<Button onClick={onSendRequest}>Send Request</Button>
{responseJson && (
{response && (
<>
<Group gap="xs">
<Text fw={500} size="sm">
Response
</Text>
{isErrorResponse(response) && (
<Badge color="red" size="sm">
Error
</Badge>
)}
</Group>
<Textarea
label="Response"
ff="monospace"
value={responseJson}
value={formatResponse(response)}
readOnly
autosize
minRows={4}
@ -221,7 +250,7 @@ export function ExperimentalFeaturesPanel({
<Table.Tbody>
{requestHistory.map((item, index) => (
<Table.Tr key={index}>
<Table.Td>{item.timestamp}</Table.Td>
<Table.Td>{formatTimestamp(item.timestamp)}</Table.Td>
<Table.Td>{item.method}</Table.Td>
<Table.Td>{item.status}</Table.Td>
<Table.Td>{formatDuration(item.durationMs)}</Table.Td>