Merge pull request #1220 from modelcontextprotocol/v2/phase-2-roots-schema-experimental
Phase 2: Refactor Roots/Schema/Experimental cluster to MCP SDK types
This commit is contained in:
commit
c25a092ed1
7 changed files with 212 additions and 150 deletions
|
|
@ -1,7 +1,7 @@
|
||||||
import { Alert, Button, Divider, Group, Stack, Text } from "@mantine/core";
|
import { Alert, Button, Divider, Group, Stack, Text } from "@mantine/core";
|
||||||
import type { ElicitRequestFormParams } from "@modelcontextprotocol/sdk/types.js";
|
import type { ElicitRequestFormParams } from "@modelcontextprotocol/sdk/types.js";
|
||||||
|
import type { JsonSchemaType } from "../../../utils/jsonUtils";
|
||||||
import { SchemaForm } from "../SchemaForm/SchemaForm";
|
import { SchemaForm } from "../SchemaForm/SchemaForm";
|
||||||
import type { JsonSchema } from "../SchemaForm/SchemaForm";
|
|
||||||
|
|
||||||
export interface ElicitationFormPanelProps {
|
export interface ElicitationFormPanelProps {
|
||||||
request: ElicitRequestFormParams;
|
request: ElicitRequestFormParams;
|
||||||
|
|
@ -38,7 +38,7 @@ export function ElicitationFormPanel({
|
||||||
<QuotedMessage>{formatQuoted(request.message)}</QuotedMessage>
|
<QuotedMessage>{formatQuoted(request.message)}</QuotedMessage>
|
||||||
<Divider />
|
<Divider />
|
||||||
<SchemaForm
|
<SchemaForm
|
||||||
schema={request.requestedSchema as JsonSchema}
|
schema={request.requestedSchema as JsonSchemaType}
|
||||||
values={values}
|
values={values}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,16 @@
|
||||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||||
import { fn } from "storybook/test";
|
import { fn } from "storybook/test";
|
||||||
|
import type {
|
||||||
|
ClientExperimentalToggle,
|
||||||
|
RequestHistoryItem,
|
||||||
|
} from "./ExperimentalFeaturesPanel";
|
||||||
import { ExperimentalFeaturesPanel } from "./ExperimentalFeaturesPanel";
|
import { ExperimentalFeaturesPanel } from "./ExperimentalFeaturesPanel";
|
||||||
|
|
||||||
|
const defaultClientToggles: ClientExperimentalToggle[] = [
|
||||||
|
{ name: "experimental/customSampling", enabled: false },
|
||||||
|
{ name: "experimental/batchRequests", enabled: true },
|
||||||
|
];
|
||||||
|
|
||||||
const meta: Meta<typeof ExperimentalFeaturesPanel> = {
|
const meta: Meta<typeof ExperimentalFeaturesPanel> = {
|
||||||
title: "Groups/ExperimentalFeaturesPanel",
|
title: "Groups/ExperimentalFeaturesPanel",
|
||||||
component: ExperimentalFeaturesPanel,
|
component: ExperimentalFeaturesPanel,
|
||||||
|
|
@ -14,13 +23,10 @@ const meta: Meta<typeof ExperimentalFeaturesPanel> = {
|
||||||
onHeaderChange: fn(),
|
onHeaderChange: fn(),
|
||||||
onCopyResponse: fn(),
|
onCopyResponse: fn(),
|
||||||
onTestCapability: fn(),
|
onTestCapability: fn(),
|
||||||
clientCapabilities: [
|
clientToggles: defaultClientToggles,
|
||||||
{ name: "experimental/customSampling", enabled: false },
|
|
||||||
{ name: "experimental/batchRequests", enabled: true },
|
|
||||||
],
|
|
||||||
customHeaders: [],
|
customHeaders: [],
|
||||||
requestHistory: [],
|
requestHistory: [],
|
||||||
requestJson: JSON.stringify(
|
requestDraft: JSON.stringify(
|
||||||
{
|
{
|
||||||
jsonrpc: "2.0",
|
jsonrpc: "2.0",
|
||||||
id: 1,
|
id: 1,
|
||||||
|
|
@ -38,14 +44,12 @@ type Story = StoryObj<typeof ExperimentalFeaturesPanel>;
|
||||||
|
|
||||||
export const WithServerCaps: Story = {
|
export const WithServerCaps: Story = {
|
||||||
args: {
|
args: {
|
||||||
serverCapabilities: [
|
serverExperimental: {
|
||||||
{
|
"experimental/streaming": {
|
||||||
name: "experimental/streaming",
|
|
||||||
description: "Supports streaming responses for long-running operations",
|
description: "Supports streaming responses for long-running operations",
|
||||||
methods: ["experimental/stream.start", "experimental/stream.cancel"],
|
methods: ["experimental/stream.start", "experimental/stream.cancel"],
|
||||||
},
|
},
|
||||||
{
|
"experimental/caching": {
|
||||||
name: "experimental/caching",
|
|
||||||
description: "Server-side response caching with TTL support",
|
description: "Server-side response caching with TTL support",
|
||||||
methods: [
|
methods: [
|
||||||
"experimental/cache.get",
|
"experimental/cache.get",
|
||||||
|
|
@ -53,37 +57,31 @@ export const WithServerCaps: Story = {
|
||||||
"experimental/cache.clear",
|
"experimental/cache.clear",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const NoServerCaps: Story = {
|
export const NoServerCaps: Story = {
|
||||||
args: {
|
args: {
|
||||||
serverCapabilities: [],
|
serverExperimental: undefined,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const WithResponse: Story = {
|
export const WithResponse: Story = {
|
||||||
args: {
|
args: {
|
||||||
serverCapabilities: [
|
serverExperimental: {
|
||||||
{
|
"experimental/echo": {
|
||||||
name: "experimental/echo",
|
|
||||||
description: "Echoes back the request for testing",
|
description: "Echoes back the request for testing",
|
||||||
methods: ["experimental/echo"],
|
|
||||||
},
|
},
|
||||||
],
|
},
|
||||||
responseJson: JSON.stringify(
|
response: {
|
||||||
{
|
jsonrpc: "2.0" as const,
|
||||||
jsonrpc: "2.0",
|
id: 1,
|
||||||
id: 1,
|
result: {
|
||||||
result: {
|
echo: "Hello from experimental endpoint",
|
||||||
echo: "Hello from experimental endpoint",
|
timestamp: "2026-03-17T10:30:00Z",
|
||||||
timestamp: "2026-03-17T10:30:00Z",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
null,
|
},
|
||||||
2,
|
|
||||||
),
|
|
||||||
customHeaders: [
|
customHeaders: [
|
||||||
{ key: "X-Custom-Auth", value: "Bearer token123" },
|
{ key: "X-Custom-Auth", value: "Bearer token123" },
|
||||||
{ key: "X-Request-Id", value: "req-abc-456" },
|
{ key: "X-Request-Id", value: "req-abc-456" },
|
||||||
|
|
@ -91,33 +89,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 = {
|
export const WithHistory: Story = {
|
||||||
args: {
|
args: {
|
||||||
serverCapabilities: [
|
serverExperimental: {
|
||||||
{
|
"experimental/metrics": {
|
||||||
name: "experimental/metrics",
|
|
||||||
description: "Exposes server metrics",
|
description: "Exposes server metrics",
|
||||||
},
|
},
|
||||||
],
|
},
|
||||||
requestHistory: [
|
requestHistory: [
|
||||||
{
|
{
|
||||||
timestamp: "2026-03-17 10:30:15",
|
timestamp: new Date("2026-03-17T10:30:15Z"),
|
||||||
method: "experimental/metrics.get",
|
method: "experimental/metrics.get",
|
||||||
status: "success",
|
status: "success",
|
||||||
durationMs: 42,
|
durationMs: 42,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
timestamp: "2026-03-17 10:29:50",
|
timestamp: new Date("2026-03-17T10:29:50Z"),
|
||||||
method: "experimental/echo",
|
method: "experimental/echo",
|
||||||
status: "success",
|
status: "success",
|
||||||
durationMs: 15,
|
durationMs: 15,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
timestamp: "2026-03-17 10:28:30",
|
timestamp: new Date("2026-03-17T10:28:30Z"),
|
||||||
method: "experimental/unknown",
|
method: "experimental/unknown",
|
||||||
status: "error",
|
status: "error",
|
||||||
durationMs: 120,
|
durationMs: 120,
|
||||||
},
|
},
|
||||||
],
|
] satisfies RequestHistoryItem[],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import {
|
import {
|
||||||
ActionIcon,
|
ActionIcon,
|
||||||
Alert,
|
Alert,
|
||||||
|
Badge,
|
||||||
Button,
|
Button,
|
||||||
Card,
|
Card,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
|
|
@ -13,36 +14,35 @@ import {
|
||||||
Textarea,
|
Textarea,
|
||||||
Title,
|
Title,
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
|
import type {
|
||||||
|
JSONRPCErrorResponse,
|
||||||
|
JSONRPCResponse,
|
||||||
|
ServerCapabilities,
|
||||||
|
} from "@modelcontextprotocol/sdk/types.js";
|
||||||
|
|
||||||
export interface ExperimentalCapability {
|
export interface ClientExperimentalToggle {
|
||||||
name: string;
|
|
||||||
description?: string;
|
|
||||||
methods?: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ClientExperimentalCapability {
|
|
||||||
name: string;
|
name: string;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface KeyValuePair {
|
export interface HeaderPair {
|
||||||
key: string;
|
key: string;
|
||||||
value: string;
|
value: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RequestHistoryItem {
|
export interface RequestHistoryItem {
|
||||||
timestamp: string;
|
timestamp: Date;
|
||||||
method: string;
|
method: string;
|
||||||
status: string;
|
status: string;
|
||||||
durationMs: number;
|
durationMs: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ExperimentalFeaturesPanelProps {
|
export interface ExperimentalFeaturesPanelProps {
|
||||||
serverCapabilities: ExperimentalCapability[];
|
serverExperimental: ServerCapabilities["experimental"];
|
||||||
clientCapabilities: ClientExperimentalCapability[];
|
clientToggles: ClientExperimentalToggle[];
|
||||||
requestJson: string;
|
requestDraft: string;
|
||||||
responseJson?: string;
|
response?: JSONRPCResponse | JSONRPCErrorResponse;
|
||||||
customHeaders: KeyValuePair[];
|
customHeaders: HeaderPair[];
|
||||||
requestHistory: RequestHistoryItem[];
|
requestHistory: RequestHistoryItem[];
|
||||||
onToggleClientCapability: (name: string, enabled: boolean) => void;
|
onToggleClientCapability: (name: string, enabled: boolean) => void;
|
||||||
onRequestChange: (json: string) => void;
|
onRequestChange: (json: string) => void;
|
||||||
|
|
@ -79,19 +79,60 @@ const RemoveIcon = ActionIcon.withProps({
|
||||||
color: "red",
|
color: "red",
|
||||||
});
|
});
|
||||||
|
|
||||||
function formatMethods(methods: string[]): string {
|
|
||||||
return `Methods: ${methods.join(", ")}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatDuration(ms: number): string {
|
function formatDuration(ms: number): string {
|
||||||
return `${ms}ms`;
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCapabilityDescription(value: object): string | undefined {
|
||||||
|
if ("description" in value && typeof value.description === "string") {
|
||||||
|
return value.description;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCapabilityMethods(value: object): string[] | undefined {
|
||||||
|
if (
|
||||||
|
"methods" in value &&
|
||||||
|
Array.isArray(value.methods) &&
|
||||||
|
value.methods.every((m: unknown) => typeof m === "string")
|
||||||
|
) {
|
||||||
|
return value.methods as string[];
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatMethods(methods: string[]): string {
|
||||||
|
return `Methods: ${methods.join(", ")}`;
|
||||||
|
}
|
||||||
|
|
||||||
export function ExperimentalFeaturesPanel({
|
export function ExperimentalFeaturesPanel({
|
||||||
serverCapabilities,
|
serverExperimental,
|
||||||
clientCapabilities,
|
clientToggles,
|
||||||
requestJson,
|
requestDraft,
|
||||||
responseJson,
|
response,
|
||||||
customHeaders,
|
customHeaders,
|
||||||
requestHistory,
|
requestHistory,
|
||||||
onToggleClientCapability,
|
onToggleClientCapability,
|
||||||
|
|
@ -103,6 +144,8 @@ export function ExperimentalFeaturesPanel({
|
||||||
onCopyResponse,
|
onCopyResponse,
|
||||||
onTestCapability,
|
onTestCapability,
|
||||||
}: ExperimentalFeaturesPanelProps) {
|
}: ExperimentalFeaturesPanelProps) {
|
||||||
|
const serverEntries = getCapabilityEntries(serverExperimental);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack gap="md">
|
<Stack gap="md">
|
||||||
<Alert color="yellow">
|
<Alert color="yellow">
|
||||||
|
|
@ -111,38 +154,42 @@ export function ExperimentalFeaturesPanel({
|
||||||
|
|
||||||
<Title order={5}>Server Experimental Capabilities:</Title>
|
<Title order={5}>Server Experimental Capabilities:</Title>
|
||||||
|
|
||||||
{serverCapabilities.length === 0 ? (
|
{serverEntries.length === 0 ? (
|
||||||
<Text c="dimmed">No experimental capabilities</Text>
|
<Text c="dimmed">No experimental capabilities</Text>
|
||||||
) : (
|
) : (
|
||||||
serverCapabilities.map((cap) => (
|
serverEntries.map(([name, value]) => {
|
||||||
<CapCard key={cap.name}>
|
const description = getCapabilityDescription(value);
|
||||||
<Stack gap="xs">
|
const methods = getCapabilityMethods(value);
|
||||||
<Text fw={600}>{cap.name}</Text>
|
return (
|
||||||
{cap.description && <HintText>{cap.description}</HintText>}
|
<CapCard key={name}>
|
||||||
{cap.methods && cap.methods.length > 0 && (
|
<Stack gap="xs">
|
||||||
<MetaText>{formatMethods(cap.methods)}</MetaText>
|
<Text fw={600}>{name}</Text>
|
||||||
)}
|
{description && <HintText>{description}</HintText>}
|
||||||
<Group>
|
{methods && methods.length > 0 && (
|
||||||
<CompactButton onClick={() => onTestCapability(cap.name)}>
|
<MetaText>{formatMethods(methods)}</MetaText>
|
||||||
Test →
|
)}
|
||||||
</CompactButton>
|
<Group>
|
||||||
</Group>
|
<CompactButton onClick={() => onTestCapability(name)}>
|
||||||
</Stack>
|
Test →
|
||||||
</CapCard>
|
</CompactButton>
|
||||||
))
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
</CapCard>
|
||||||
|
);
|
||||||
|
})
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Divider />
|
<Divider />
|
||||||
|
|
||||||
<Title order={5}>Client Experimental Capabilities:</Title>
|
<Title order={5}>Client Experimental Capabilities:</Title>
|
||||||
|
|
||||||
{clientCapabilities.map((clientCap) => (
|
{clientToggles.map((toggle) => (
|
||||||
<Checkbox
|
<Checkbox
|
||||||
key={clientCap.name}
|
key={toggle.name}
|
||||||
label={clientCap.name}
|
label={toggle.name}
|
||||||
checked={clientCap.enabled}
|
checked={toggle.enabled}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
onToggleClientCapability(clientCap.name, e.currentTarget.checked)
|
onToggleClientCapability(toggle.name, e.currentTarget.checked)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|
@ -182,7 +229,7 @@ export function ExperimentalFeaturesPanel({
|
||||||
<Textarea
|
<Textarea
|
||||||
label="Request"
|
label="Request"
|
||||||
ff="monospace"
|
ff="monospace"
|
||||||
value={requestJson}
|
value={requestDraft}
|
||||||
onChange={(e) => onRequestChange(e.currentTarget.value)}
|
onChange={(e) => onRequestChange(e.currentTarget.value)}
|
||||||
autosize
|
autosize
|
||||||
minRows={6}
|
minRows={6}
|
||||||
|
|
@ -190,12 +237,21 @@ export function ExperimentalFeaturesPanel({
|
||||||
|
|
||||||
<Button onClick={onSendRequest}>Send Request</Button>
|
<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
|
<Textarea
|
||||||
label="Response"
|
|
||||||
ff="monospace"
|
ff="monospace"
|
||||||
value={responseJson}
|
value={formatResponse(response)}
|
||||||
readOnly
|
readOnly
|
||||||
autosize
|
autosize
|
||||||
minRows={4}
|
minRows={4}
|
||||||
|
|
@ -221,7 +277,7 @@ export function ExperimentalFeaturesPanel({
|
||||||
<Table.Tbody>
|
<Table.Tbody>
|
||||||
{requestHistory.map((item, index) => (
|
{requestHistory.map((item, index) => (
|
||||||
<Table.Tr key={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.method}</Table.Td>
|
||||||
<Table.Td>{item.status}</Table.Td>
|
<Table.Td>{item.status}</Table.Td>
|
||||||
<Table.Td>{formatDuration(item.durationMs)}</Table.Td>
|
<Table.Td>{formatDuration(item.durationMs)}</Table.Td>
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,8 @@ import type {
|
||||||
ElicitRequest,
|
ElicitRequest,
|
||||||
ElicitRequestFormParams,
|
ElicitRequestFormParams,
|
||||||
} from "@modelcontextprotocol/sdk/types.js";
|
} from "@modelcontextprotocol/sdk/types.js";
|
||||||
|
import type { JsonSchemaType } from "../../../utils/jsonUtils";
|
||||||
import { SchemaForm } from "../SchemaForm/SchemaForm";
|
import { SchemaForm } from "../SchemaForm/SchemaForm";
|
||||||
import type { JsonSchema } from "../SchemaForm/SchemaForm";
|
|
||||||
|
|
||||||
export interface InlineElicitationRequestProps {
|
export interface InlineElicitationRequestProps {
|
||||||
request: ElicitRequest["params"];
|
request: ElicitRequest["params"];
|
||||||
|
|
@ -89,7 +89,7 @@ export function InlineElicitationRequest({
|
||||||
|
|
||||||
{isFormMode(request) && (
|
{isFormMode(request) && (
|
||||||
<SchemaForm
|
<SchemaForm
|
||||||
schema={request.requestedSchema as JsonSchema}
|
schema={request.requestedSchema as JsonSchemaType}
|
||||||
values={values ?? {}}
|
values={values ?? {}}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,20 @@
|
||||||
|
import type { Root } from "@modelcontextprotocol/sdk/types.js";
|
||||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||||
import { fn } from "storybook/test";
|
import { fn } from "storybook/test";
|
||||||
import { RootsTable } from "./RootsTable";
|
import { RootsTable } from "./RootsTable";
|
||||||
|
|
||||||
|
const sampleRoots: Root[] = [
|
||||||
|
{ name: "Project Source", uri: "file:///home/user/project/src" },
|
||||||
|
{ name: "Configuration", uri: "file:///home/user/project/config" },
|
||||||
|
{ name: "Documentation", uri: "file:///home/user/project/docs" },
|
||||||
|
];
|
||||||
|
|
||||||
const meta: Meta<typeof RootsTable> = {
|
const meta: Meta<typeof RootsTable> = {
|
||||||
title: "Groups/RootsTable",
|
title: "Groups/RootsTable",
|
||||||
component: RootsTable,
|
component: RootsTable,
|
||||||
args: {
|
args: {
|
||||||
onRemoveRoot: fn(),
|
onRemoveRoot: fn(),
|
||||||
onNewRootNameChange: fn(),
|
onNewRootDraftChange: fn(),
|
||||||
onNewRootPathChange: fn(),
|
|
||||||
onAddRoot: fn(),
|
onAddRoot: fn(),
|
||||||
onBrowse: fn(),
|
onBrowse: fn(),
|
||||||
},
|
},
|
||||||
|
|
@ -19,29 +25,25 @@ type Story = StoryObj<typeof RootsTable>;
|
||||||
|
|
||||||
export const WithRoots: Story = {
|
export const WithRoots: Story = {
|
||||||
args: {
|
args: {
|
||||||
roots: [
|
roots: sampleRoots,
|
||||||
{ name: "Project Source", uri: "file:///home/user/project/src" },
|
newRootDraft: { name: "", uri: "" },
|
||||||
{ name: "Configuration", uri: "file:///home/user/project/config" },
|
|
||||||
{ name: "Documentation", uri: "file:///home/user/project/docs" },
|
|
||||||
],
|
|
||||||
newRootName: "",
|
|
||||||
newRootPath: "",
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Empty: Story = {
|
export const Empty: Story = {
|
||||||
args: {
|
args: {
|
||||||
roots: [],
|
roots: [],
|
||||||
newRootName: "",
|
newRootDraft: { name: "", uri: "" },
|
||||||
newRootPath: "",
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const AddingNew: Story = {
|
export const AddingNew: Story = {
|
||||||
args: {
|
args: {
|
||||||
roots: [{ name: "Project Source", uri: "file:///home/user/project/src" }],
|
roots: [sampleRoots[0]],
|
||||||
newRootName: "Test Data",
|
newRootDraft: {
|
||||||
newRootPath: "/home/user/project/test-data",
|
name: "Test Data",
|
||||||
|
uri: "file:///home/user/project/test-data",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -51,11 +53,13 @@ export const ManyRoots: Story = {
|
||||||
{ name: "Source Code", uri: "file:///home/user/project/src" },
|
{ name: "Source Code", uri: "file:///home/user/project/src" },
|
||||||
{ name: "Configuration", uri: "file:///home/user/project/config" },
|
{ name: "Configuration", uri: "file:///home/user/project/config" },
|
||||||
{ name: "Documentation", uri: "file:///home/user/project/docs" },
|
{ name: "Documentation", uri: "file:///home/user/project/docs" },
|
||||||
{ name: "Test Fixtures", uri: "file:///home/user/project/test/fixtures" },
|
{
|
||||||
|
name: "Test Fixtures",
|
||||||
|
uri: "file:///home/user/project/test/fixtures",
|
||||||
|
},
|
||||||
{ name: "Build Output", uri: "file:///home/user/project/dist" },
|
{ name: "Build Output", uri: "file:///home/user/project/dist" },
|
||||||
{ name: "Scripts", uri: "file:///home/user/project/scripts" },
|
{ name: "Scripts", uri: "file:///home/user/project/scripts" },
|
||||||
],
|
],
|
||||||
newRootName: "",
|
newRootDraft: { name: "", uri: "" },
|
||||||
newRootPath: "",
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -10,19 +10,15 @@ import {
|
||||||
TextInput,
|
TextInput,
|
||||||
Title,
|
Title,
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
|
import type { Root } from "@modelcontextprotocol/sdk/types.js";
|
||||||
|
|
||||||
export interface RootEntry {
|
export type RootDraft = { name: string; uri: string };
|
||||||
name: string;
|
|
||||||
uri: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface RootsTableProps {
|
export interface RootsTableProps {
|
||||||
roots: RootEntry[];
|
roots: Root[];
|
||||||
newRootName: string;
|
newRootDraft: RootDraft;
|
||||||
newRootPath: string;
|
|
||||||
onRemoveRoot: (uri: string) => void;
|
onRemoveRoot: (uri: string) => void;
|
||||||
onNewRootNameChange: (name: string) => void;
|
onNewRootDraftChange: (draft: RootDraft) => void;
|
||||||
onNewRootPathChange: (path: string) => void;
|
|
||||||
onAddRoot: () => void;
|
onAddRoot: () => void;
|
||||||
onBrowse: () => void;
|
onBrowse: () => void;
|
||||||
}
|
}
|
||||||
|
|
@ -44,11 +40,9 @@ const AddRootButton = Button.withProps({
|
||||||
|
|
||||||
export function RootsTable({
|
export function RootsTable({
|
||||||
roots,
|
roots,
|
||||||
newRootName,
|
newRootDraft,
|
||||||
newRootPath,
|
|
||||||
onRemoveRoot,
|
onRemoveRoot,
|
||||||
onNewRootNameChange,
|
onNewRootDraftChange,
|
||||||
onNewRootPathChange,
|
|
||||||
onAddRoot,
|
onAddRoot,
|
||||||
onBrowse,
|
onBrowse,
|
||||||
}: RootsTableProps) {
|
}: RootsTableProps) {
|
||||||
|
|
@ -89,13 +83,23 @@ export function RootsTable({
|
||||||
<Title order={5}>Add New Root:</Title>
|
<Title order={5}>Add New Root:</Title>
|
||||||
<TextInput
|
<TextInput
|
||||||
label="Name"
|
label="Name"
|
||||||
value={newRootName}
|
value={newRootDraft.name}
|
||||||
onChange={(e) => onNewRootNameChange(e.currentTarget.value)}
|
onChange={(e) =>
|
||||||
|
onNewRootDraftChange({
|
||||||
|
...newRootDraft,
|
||||||
|
name: e.currentTarget.value,
|
||||||
|
})
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<TextInput
|
<TextInput
|
||||||
label="Path"
|
label="URI"
|
||||||
value={newRootPath}
|
value={newRootDraft.uri}
|
||||||
onChange={(e) => onNewRootPathChange(e.currentTarget.value)}
|
onChange={(e) =>
|
||||||
|
onNewRootDraftChange({
|
||||||
|
...newRootDraft,
|
||||||
|
uri: e.currentTarget.value,
|
||||||
|
})
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<Group justify="flex-end">
|
<Group justify="flex-end">
|
||||||
<Button variant="light" onClick={onBrowse}>
|
<Button variant="light" onClick={onBrowse}>
|
||||||
|
|
|
||||||
|
|
@ -8,24 +8,7 @@ import {
|
||||||
Text,
|
Text,
|
||||||
TextInput,
|
TextInput,
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
|
import type { JsonSchemaType } from "../../../utils/jsonUtils";
|
||||||
export interface JsonSchema {
|
|
||||||
type?: string;
|
|
||||||
properties?: Record<string, JsonSchema>;
|
|
||||||
required?: string[];
|
|
||||||
items?: JsonSchema;
|
|
||||||
enum?: string[];
|
|
||||||
oneOf?: Array<{ const: string; title?: string }>;
|
|
||||||
anyOf?: Array<{ const: string; title?: string }>;
|
|
||||||
default?: unknown;
|
|
||||||
description?: string;
|
|
||||||
title?: string;
|
|
||||||
minimum?: number;
|
|
||||||
maximum?: number;
|
|
||||||
minLength?: number;
|
|
||||||
maxLength?: number;
|
|
||||||
format?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const FieldLabel = Text.withProps({
|
const FieldLabel = Text.withProps({
|
||||||
fw: 500,
|
fw: 500,
|
||||||
|
|
@ -42,20 +25,20 @@ function serializeJson(value: unknown): string {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SchemaFormProps {
|
export interface SchemaFormProps {
|
||||||
schema: JsonSchema;
|
schema: JsonSchemaType;
|
||||||
values: Record<string, unknown>;
|
values: Record<string, unknown>;
|
||||||
onChange: (values: Record<string, unknown>) => void;
|
onChange: (values: Record<string, unknown>) => void;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDefaultValue(fieldSchema: JsonSchema): unknown {
|
function getDefaultValue(fieldSchema: JsonSchemaType): unknown {
|
||||||
if (fieldSchema.default !== undefined) {
|
if (fieldSchema.default !== undefined) {
|
||||||
return fieldSchema.default;
|
return fieldSchema.default;
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveValue(value: unknown, fieldSchema: JsonSchema): unknown {
|
function resolveValue(value: unknown, fieldSchema: JsonSchemaType): unknown {
|
||||||
if (value !== undefined) {
|
if (value !== undefined) {
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
@ -75,7 +58,7 @@ export function SchemaForm({
|
||||||
onChange({ ...values, [fieldName]: fieldValue });
|
onChange({ ...values, [fieldName]: fieldValue });
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderField(fieldName: string, fieldSchema: JsonSchema) {
|
function renderField(fieldName: string, fieldSchema: JsonSchemaType) {
|
||||||
const isRequired = requiredFields.includes(fieldName);
|
const isRequired = requiredFields.includes(fieldName);
|
||||||
const label = fieldSchema.title ?? fieldName;
|
const label = fieldSchema.title ?? fieldName;
|
||||||
const description = fieldSchema.description;
|
const description = fieldSchema.description;
|
||||||
|
|
@ -100,8 +83,8 @@ export function SchemaForm({
|
||||||
// string with oneOf
|
// string with oneOf
|
||||||
if (fieldSchema.type === "string" && fieldSchema.oneOf) {
|
if (fieldSchema.type === "string" && fieldSchema.oneOf) {
|
||||||
const data = fieldSchema.oneOf.map((item) => ({
|
const data = fieldSchema.oneOf.map((item) => ({
|
||||||
value: item.const,
|
value: String(item.const ?? ""),
|
||||||
label: item.title ?? item.const,
|
label: item.title ?? String(item.const ?? ""),
|
||||||
}));
|
}));
|
||||||
return (
|
return (
|
||||||
<Select
|
<Select
|
||||||
|
|
@ -175,8 +158,8 @@ export function SchemaForm({
|
||||||
// array with items having anyOf
|
// array with items having anyOf
|
||||||
if (fieldSchema.type === "array" && fieldSchema.items?.anyOf) {
|
if (fieldSchema.type === "array" && fieldSchema.items?.anyOf) {
|
||||||
const data = fieldSchema.items.anyOf.map((item) => ({
|
const data = fieldSchema.items.anyOf.map((item) => ({
|
||||||
value: item.const,
|
value: String(item.const ?? ""),
|
||||||
label: item.title ?? item.const,
|
label: item.title ?? String(item.const ?? ""),
|
||||||
}));
|
}));
|
||||||
return (
|
return (
|
||||||
<MultiSelect
|
<MultiSelect
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue