feat(experimental): derive client toggles from ClientCapabilities (#1252)
* feat(experimental): derive client toggles from ClientCapabilities (#1240) Closes the Phase 5 audit partial: ExperimentalFeaturesPanel now accepts clientExperimental: ClientCapabilities['experimental'] (the SDK record shape) instead of a parallel clientToggles array of UI metadata. Adopts Option B from the design question: toggle metadata (label, description) lives in a static CLIENT_EXPERIMENTAL_TOGGLE_METADATA map inside the component file. Iteration is over Object.keys of the live record, so presence-in-record means enabled. Unknown keys (in record but not in the metadata map) render with the raw key as label and no description, per AC. Renders one Checkbox per active toggle plus an indented HintText description when metadata is known. Callback signature unchanged. Stories cover empty, all-known, and mixed-known-and-unknown cases. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Render full toggle universe, derive checked from record Address in-review feedback: with the strict-AC iteration over Object.keys(clientExperimental), unchecking a toggle made its row disappear (no schema slot for "disabled but available"), so the user couldn't re-enable a toggle they just turned off. Refines Option B: render the union of metadata-map keys and record keys. Each row's checked state is derived from name-in-record. Result: known toggles always render, regardless of record state; toggling is fully reversible from the UI; unknown record keys still surface with a raw label. Story rename: NoClientToggles → NoneEnabled, AllKnownClientToggles → AllKnownEnabled, MixedKnownAndUnknownToggles → MixedKnownAndUnknown — the new names describe state, not record shape. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
c04f96ffb8
commit
39e1c67401
2 changed files with 81 additions and 27 deletions
|
|
@ -1,16 +1,8 @@
|
|||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { fn } from "storybook/test";
|
||||
import type {
|
||||
ClientExperimentalToggle,
|
||||
RequestHistoryItem,
|
||||
} from "./ExperimentalFeaturesPanel";
|
||||
import type { 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,
|
||||
|
|
@ -23,7 +15,9 @@ const meta: Meta<typeof ExperimentalFeaturesPanel> = {
|
|||
onHeaderChange: fn(),
|
||||
onCopyResponse: fn(),
|
||||
onTestCapability: fn(),
|
||||
clientToggles: defaultClientToggles,
|
||||
clientExperimental: {
|
||||
"experimental/batchRequests": {},
|
||||
},
|
||||
customHeaders: [],
|
||||
requestHistory: [],
|
||||
requestDraft: JSON.stringify(
|
||||
|
|
@ -67,6 +61,30 @@ export const NoServerCaps: Story = {
|
|||
},
|
||||
};
|
||||
|
||||
export const NoneEnabled: Story = {
|
||||
args: {
|
||||
clientExperimental: undefined,
|
||||
},
|
||||
};
|
||||
|
||||
export const AllKnownEnabled: Story = {
|
||||
args: {
|
||||
clientExperimental: {
|
||||
"experimental/customSampling": {},
|
||||
"experimental/batchRequests": {},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const MixedKnownAndUnknown: Story = {
|
||||
args: {
|
||||
clientExperimental: {
|
||||
"experimental/customSampling": {},
|
||||
"experimental/futureFeatureXyz": {},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const WithResponse: Story = {
|
||||
args: {
|
||||
serverExperimental: {
|
||||
|
|
|
|||
|
|
@ -15,16 +15,12 @@ import {
|
|||
Title,
|
||||
} from "@mantine/core";
|
||||
import type {
|
||||
ClientCapabilities,
|
||||
JSONRPCErrorResponse,
|
||||
JSONRPCResponse,
|
||||
ServerCapabilities,
|
||||
} from "@modelcontextprotocol/sdk/types.js";
|
||||
|
||||
export interface ClientExperimentalToggle {
|
||||
name: string;
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
export interface HeaderPair {
|
||||
key: string;
|
||||
value: string;
|
||||
|
|
@ -39,7 +35,7 @@ export interface RequestHistoryItem {
|
|||
|
||||
export interface ExperimentalFeaturesPanelProps {
|
||||
serverExperimental: ServerCapabilities["experimental"];
|
||||
clientToggles: ClientExperimentalToggle[];
|
||||
clientExperimental: ClientCapabilities["experimental"];
|
||||
requestDraft: string;
|
||||
response?: JSONRPCResponse | JSONRPCErrorResponse;
|
||||
customHeaders: HeaderPair[];
|
||||
|
|
@ -54,6 +50,27 @@ export interface ExperimentalFeaturesPanelProps {
|
|||
onTestCapability: (name: string) => void;
|
||||
}
|
||||
|
||||
interface ClientToggleMetadata {
|
||||
label: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
const CLIENT_EXPERIMENTAL_TOGGLE_METADATA: Record<
|
||||
string,
|
||||
ClientToggleMetadata
|
||||
> = {
|
||||
"experimental/customSampling": {
|
||||
label: "Custom sampling",
|
||||
description:
|
||||
"Allow servers to invoke client-defined sampling strategies via experimental/sampling.* methods.",
|
||||
},
|
||||
"experimental/batchRequests": {
|
||||
label: "Batch requests",
|
||||
description:
|
||||
"Send multiple JSON-RPC requests in a single call and receive a batched response.",
|
||||
},
|
||||
};
|
||||
|
||||
const HintText = Text.withProps({
|
||||
size: "sm",
|
||||
c: "dimmed",
|
||||
|
|
@ -128,9 +145,20 @@ function formatMethods(methods: string[]): string {
|
|||
return `Methods: ${methods.join(", ")}`;
|
||||
}
|
||||
|
||||
function getClientToggleNames(
|
||||
clientExperimental: ClientCapabilities["experimental"],
|
||||
): string[] {
|
||||
const known = Object.keys(CLIENT_EXPERIMENTAL_TOGGLE_METADATA);
|
||||
const recordKeys = Object.keys(clientExperimental ?? {});
|
||||
const unknown = recordKeys.filter(
|
||||
(name) => !(name in CLIENT_EXPERIMENTAL_TOGGLE_METADATA),
|
||||
);
|
||||
return [...known, ...unknown];
|
||||
}
|
||||
|
||||
export function ExperimentalFeaturesPanel({
|
||||
serverExperimental,
|
||||
clientToggles,
|
||||
clientExperimental,
|
||||
requestDraft,
|
||||
response,
|
||||
customHeaders,
|
||||
|
|
@ -183,16 +211,24 @@ export function ExperimentalFeaturesPanel({
|
|||
|
||||
<Title order={5}>Client Experimental Capabilities:</Title>
|
||||
|
||||
{clientToggles.map((toggle) => (
|
||||
<Checkbox
|
||||
key={toggle.name}
|
||||
label={toggle.name}
|
||||
checked={toggle.enabled}
|
||||
onChange={(e) =>
|
||||
onToggleClientCapability(toggle.name, e.currentTarget.checked)
|
||||
}
|
||||
/>
|
||||
))}
|
||||
{getClientToggleNames(clientExperimental).map((name) => {
|
||||
const metadata = CLIENT_EXPERIMENTAL_TOGGLE_METADATA[name];
|
||||
const enabled = clientExperimental?.[name] !== undefined;
|
||||
return (
|
||||
<Stack key={name} gap={4}>
|
||||
<Checkbox
|
||||
label={metadata?.label ?? name}
|
||||
checked={enabled}
|
||||
onChange={(e) =>
|
||||
onToggleClientCapability(name, e.currentTarget.checked)
|
||||
}
|
||||
/>
|
||||
{metadata?.description && (
|
||||
<HintText pl="xl">{metadata.description}</HintText>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
})}
|
||||
|
||||
<Divider />
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue