Some checks failed
Run install, format, lint, build, and test on every push / build (push) Has been cancelled
Run install, format, lint, build, and test on every push / publish (push) Has been cancelled
Run install, format, lint, build, and test on every push / publish-github-container-registry (push) Has been cancelled
* History: MRTR conversation grouping + modern message vocabulary Teach the Protocol/History view the modern (2026-07-28) message vocabulary, correlating it off the frames the transport already captures — no change to how MRTR is driven (manual driving is deferred to #1704). - Correlation helpers in protocolUtils: extractResultType, extractRequestState, extractSubscriptionId, isModernFrame, and groupProtocolEntries — clustering the contiguous rounds of one logical operation (original call -> input_required -> retried call with a new id -> final result) that share a requestState token. - New MrtrConversation group renders that cluster as one expandable unit with a status derived from the final round, reusing ProtocolEntry for each round. - ProtocolEntry: resultType badge (input required / complete), a "modern" frame badge for server/discover, subscriptions/listen, and the acknowledged notification, and a copyable subscriptionId. - ProtocolListPanel folds each section through the grouping pass and shows an era badge in the header. Era is threaded from the negotiated protocolEra (App -> InspectorView -> ProtocolScreen -> panel) and never inferred from frames, respecting the transcript-classification caveat. - Shared EraBadge element (+ eraUtils); ConnectionInfoContent reuses it. - test-servers/configs/modern-mrtr-http.json serves an elicitation tool over the modern leg for eyeballing a real MRTR round-trip. - Pre-bundle @modelcontextprotocol/client in optimizeDeps.include so the Storybook vitest project doesn't re-optimize mid-run once protocolUtils pulls the client barrel into the browser graph. Closes #1627 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5 * test-servers: real MRTR tool + live-verified History grouping While driving the modern flow in the browser, the elicitation-based config proved unable to produce MRTR: the modern (2026-07-28) SDK server rejects `server.elicitInput` — server→client requests aren't allowed on that leg; a tool must instead return `inputRequired(...)`. So the shipped `modern-mrtr-http.json` (and its README claim) were wrong. - Add a genuine MRTR tool preset (`mrtr_confirm` / `createMrtrTool`): its handler returns `inputRequired({ inputRequests: { confirm: elicit(...) } })` on the first call and the final result once the client echoes the answer back on the retry. - Teach the composable test-server to support it: the tool wrapper forwards an `InputRequiredResult` verbatim (instead of coercing it to a CallToolResult), and `toHandlerExtra` surfaces `inputResponses`/`requestState` so a handler can branch between the original call and the retry. - Point `modern-mrtr-http.json` at the new preset; correct the README to describe the real mechanism. - Add an integration test that drives the full round-trip through InspectorClient (auto-fulfilling the embedded elicitation) and asserts the final result is only reachable after the input_required round + retry — regression cover that would have caught the original config bug. Verified live in the browser: the History view renders the round-trip as one grouped "MRTR conversation" (Round 1 input_required → Round 2 complete), keyed by the real requestState, under the Modern era badge. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5 * Address review notes: dedupe status badge, unique MRTR token, import alias Non-blocking review feedback on #1705: - ProtocolEntry: suppress the redundant green "OK" status badge when a `resultType` badge already conveys the outcome (a modern success shows only `complete`/`input required`). Errors/pending have no `resultType`, so their status badge still shows. - createMrtrTool: mint a unique `requestState` per original call (closure counter) so two same-action invocations don't fold into one conversation during manual eyeballing. Real SDK tokens are already unique per operation. - ProtocolEntry: align the `MessageEntry` import to the `@inspector/core` alias used by its siblings (was a deep relative path). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5 * Review round 2: green `complete` badge, lighter embedded MRTR container Non-blocking observations from the second review: - ProtocolEntry: `resultType` "complete" now renders green (was gray) so a modern success keeps the at-a-glance green success affordance a legacy "OK" had, now that the redundant OK badge is suppressed. `input_required` stays yellow. - MrtrConversation: in the narrow monitoring (embedded) column, drop the outer container border and tighten padding — the per-round entries are already bordered cards, so this avoids a busy border-in-border nesting. The full-width view keeps the bordered surface that sets a conversation apart. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5 * Review round 3: module-scope MRTR counter; revert embedded border change - test-servers: hoist the MRTR `requestState` counter to module scope. The modern (2026-07-28) leg is stateless — `createMcpHandler` rebuilds the server (and every tool closure) per request — so the previous per-closure counter reset to 0 on every request and every token ended in `:1`, defeating its purpose. A module-level counter survives the per-request factory recreation and increments monotonically across the process, so same-action invocations get distinct tokens and don't fold into one conversation. (Review finding.) - MrtrConversation: revert the embedded borderless/tighter-padding change — per maintainer feedback from manual testing, dropping the border didn't improve the narrow-column visual, so the group keeps its border in all layouts. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5 --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
225 lines
5.5 KiB
TypeScript
225 lines
5.5 KiB
TypeScript
import { useState } from "react";
|
|
import type { ComponentProps } from "react";
|
|
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
import type { MessageEntry } from "../../../../../../core/mcp/types.js";
|
|
import { expect, fn, screen, userEvent, within } from "storybook/test";
|
|
import { ProtocolScreen } from "./ProtocolScreen";
|
|
import { EMPTY_PROTOCOL_UI } from "../screenUiState";
|
|
|
|
// ProtocolScreen is controlled (search text + method filter live in the parent
|
|
// as one `ui` object — see #1417). This wrapper holds that state so the
|
|
// play-driven filter selection and clear-all reset are observable, mirroring
|
|
// how App owns the state.
|
|
function StatefulProtocolScreen(args: ComponentProps<typeof ProtocolScreen>) {
|
|
const [ui, setUi] = useState({ ...EMPTY_PROTOCOL_UI, ...args.ui });
|
|
return <ProtocolScreen {...args} ui={ui} onUiChange={setUi} />;
|
|
}
|
|
|
|
const meta: Meta<typeof ProtocolScreen> = {
|
|
title: "Screens/ProtocolScreen",
|
|
component: ProtocolScreen,
|
|
parameters: { layout: "fullscreen" },
|
|
args: {
|
|
pinnedIds: new Set<string>(),
|
|
onClearAll: fn(),
|
|
onExport: fn(),
|
|
onReplay: fn(),
|
|
onTogglePin: fn(),
|
|
ui: EMPTY_PROTOCOL_UI,
|
|
onUiChange: fn(),
|
|
sortDirection: "newest-first",
|
|
onSortChange: fn(),
|
|
compact: true,
|
|
onToggleCompact: fn(),
|
|
},
|
|
render: (args) => <StatefulProtocolScreen {...args} />,
|
|
};
|
|
|
|
export default meta;
|
|
type Story = StoryObj<typeof ProtocolScreen>;
|
|
|
|
const sampleEntries: MessageEntry[] = [
|
|
{
|
|
id: "req-1",
|
|
timestamp: new Date("2026-03-17T10:00:00Z"),
|
|
direction: "request",
|
|
message: {
|
|
jsonrpc: "2.0",
|
|
id: 1,
|
|
method: "tools/call",
|
|
params: { name: "send_message", arguments: { message: "Hello, world!" } },
|
|
},
|
|
response: {
|
|
jsonrpc: "2.0",
|
|
id: 1,
|
|
result: {
|
|
content: [{ type: "text", text: "Message sent successfully" }],
|
|
},
|
|
},
|
|
duration: 120,
|
|
},
|
|
{
|
|
id: "req-2",
|
|
timestamp: new Date("2026-03-17T10:01:00Z"),
|
|
direction: "request",
|
|
message: {
|
|
jsonrpc: "2.0",
|
|
id: 2,
|
|
method: "resources/read",
|
|
params: { uri: "file:///config.json" },
|
|
},
|
|
response: {
|
|
jsonrpc: "2.0",
|
|
id: 2,
|
|
result: {
|
|
contents: [{ uri: "file:///config.json", text: "{}" }],
|
|
},
|
|
},
|
|
duration: 45,
|
|
},
|
|
{
|
|
id: "req-3",
|
|
timestamp: new Date("2026-03-17T10:02:00Z"),
|
|
direction: "request",
|
|
message: {
|
|
jsonrpc: "2.0",
|
|
id: 3,
|
|
method: "tools/call",
|
|
params: { name: "delete_records", arguments: { ids: [1, 2, 3] } },
|
|
},
|
|
response: {
|
|
jsonrpc: "2.0",
|
|
id: 3,
|
|
error: { code: -32000, message: "Permission denied" },
|
|
},
|
|
duration: 350,
|
|
},
|
|
{
|
|
id: "req-4",
|
|
timestamp: new Date("2026-03-17T09:30:00Z"),
|
|
direction: "request",
|
|
message: {
|
|
jsonrpc: "2.0",
|
|
id: 4,
|
|
method: "tools/list",
|
|
},
|
|
response: {
|
|
jsonrpc: "2.0",
|
|
id: 4,
|
|
result: { tools: [] },
|
|
},
|
|
duration: 80,
|
|
},
|
|
{
|
|
id: "req-5",
|
|
timestamp: new Date("2026-03-17T09:35:00Z"),
|
|
direction: "request",
|
|
message: {
|
|
jsonrpc: "2.0",
|
|
id: 5,
|
|
method: "prompts/get",
|
|
params: { name: "greeting" },
|
|
},
|
|
response: {
|
|
jsonrpc: "2.0",
|
|
id: 5,
|
|
result: {
|
|
messages: [{ role: "user", content: { type: "text", text: "Hello!" } }],
|
|
},
|
|
},
|
|
duration: 60,
|
|
},
|
|
];
|
|
|
|
export const WithEntries: Story = {
|
|
args: {
|
|
entries: sampleEntries,
|
|
pinnedIds: new Set(["req-4", "req-5"]),
|
|
},
|
|
};
|
|
|
|
// A modern (2026-07-28) connection: the era badge labels the traffic and an
|
|
// MRTR round-trip renders as a single grouped conversation.
|
|
const modernEntries: MessageEntry[] = [
|
|
{
|
|
id: "mrtr-orig",
|
|
timestamp: new Date("2026-07-28T10:00:00Z"),
|
|
direction: "request",
|
|
origin: "client",
|
|
message: {
|
|
jsonrpc: "2.0",
|
|
id: 10,
|
|
method: "tools/call",
|
|
params: { name: "book_flight", arguments: { destination: "SFO" } },
|
|
},
|
|
response: {
|
|
jsonrpc: "2.0",
|
|
id: 10,
|
|
result: {
|
|
resultType: "input_required",
|
|
requestState: "opaque-token",
|
|
inputRequests: { "1": { method: "elicitation/create", params: {} } },
|
|
},
|
|
},
|
|
},
|
|
{
|
|
id: "mrtr-retry",
|
|
timestamp: new Date("2026-07-28T10:00:05Z"),
|
|
direction: "request",
|
|
origin: "client",
|
|
message: {
|
|
jsonrpc: "2.0",
|
|
id: 11,
|
|
method: "tools/call",
|
|
params: {
|
|
name: "book_flight",
|
|
requestState: "opaque-token",
|
|
inputResponses: { "1": { content: { name: "Ada" } } },
|
|
},
|
|
},
|
|
response: {
|
|
jsonrpc: "2.0",
|
|
id: 11,
|
|
result: {
|
|
resultType: "complete",
|
|
content: [{ type: "text", text: "Booked" }],
|
|
},
|
|
},
|
|
},
|
|
];
|
|
|
|
export const ModernEraWithMrtr: Story = {
|
|
args: {
|
|
entries: modernEntries,
|
|
protocolEra: "modern",
|
|
compact: false,
|
|
},
|
|
};
|
|
|
|
export const Empty: Story = {
|
|
args: {
|
|
entries: [],
|
|
},
|
|
};
|
|
|
|
export const MethodFilterClearedOnClearAll: Story = {
|
|
args: {
|
|
entries: sampleEntries,
|
|
pinnedIds: new Set<string>(),
|
|
},
|
|
play: async ({ canvasElement }) => {
|
|
const canvas = within(canvasElement);
|
|
const filter = canvas.getByPlaceholderText(
|
|
"All methods",
|
|
) as HTMLInputElement;
|
|
|
|
await userEvent.click(filter);
|
|
await userEvent.click(
|
|
await screen.findByRole("option", { name: "tools/call" }),
|
|
);
|
|
await expect(filter).toHaveValue("tools/call");
|
|
|
|
await userEvent.click(canvas.getByRole("button", { name: /^clear$/i }));
|
|
await expect(filter).toHaveValue("");
|
|
},
|
|
};
|