History: MRTR conversation grouping + modern message vocabulary (#1705)
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
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>
This commit is contained in:
parent
e06c0a45f7
commit
73d9b181fe
4 changed files with 1785 additions and 58 deletions
|
|
@ -0,0 +1,22 @@
|
|||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { EraBadge } from "./EraBadge";
|
||||
|
||||
const meta: Meta<typeof EraBadge> = {
|
||||
title: "Elements/EraBadge",
|
||||
component: EraBadge,
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof EraBadge>;
|
||||
|
||||
export const Modern: Story = {
|
||||
args: { era: "modern" },
|
||||
};
|
||||
|
||||
export const Legacy: Story = {
|
||||
args: { era: "legacy" },
|
||||
};
|
||||
|
||||
export const NotConnected: Story = {
|
||||
args: { era: undefined },
|
||||
};
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
import { describe, it, expect } from "vitest";
|
||||
import { renderWithMantine, screen } from "../../../test/renderWithMantine";
|
||||
import { EraBadge } from "./EraBadge";
|
||||
import { formatEra, isModernEra } from "./eraUtils";
|
||||
|
||||
describe("isModernEra / formatEra", () => {
|
||||
it("treats only 'modern' as the modern era", () => {
|
||||
expect(isModernEra("modern")).toBe(true);
|
||||
expect(isModernEra("legacy")).toBe(false);
|
||||
expect(isModernEra(undefined)).toBe(false);
|
||||
});
|
||||
|
||||
it("labels the era", () => {
|
||||
expect(formatEra("modern")).toBe("Modern");
|
||||
expect(formatEra("legacy")).toBe("Legacy");
|
||||
expect(formatEra(undefined)).toBe("Legacy");
|
||||
});
|
||||
});
|
||||
|
||||
describe("EraBadge", () => {
|
||||
it("renders Modern for a modern connection", () => {
|
||||
renderWithMantine(<EraBadge era="modern" />);
|
||||
expect(screen.getByText("Modern")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders Legacy for a legacy connection", () => {
|
||||
renderWithMantine(<EraBadge era="legacy" />);
|
||||
expect(screen.getByText("Legacy")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders Legacy when the era is undefined", () => {
|
||||
renderWithMantine(<EraBadge era={undefined} />);
|
||||
expect(screen.getByText("Legacy")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
19
clients/web/src/components/elements/EraBadge/EraBadge.tsx
Normal file
19
clients/web/src/components/elements/EraBadge/EraBadge.tsx
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import { Badge } from "@mantine/core";
|
||||
import type { ProtocolEra } from "@modelcontextprotocol/client";
|
||||
import { formatEra, isModernEra } from "./eraUtils";
|
||||
|
||||
export interface EraBadgeProps {
|
||||
/** The negotiated protocol era; `undefined` renders as Legacy. */
|
||||
era: ProtocolEra | undefined;
|
||||
}
|
||||
|
||||
// Labels a connection's negotiated protocol era (SEP §7.8). Feed it from
|
||||
// connection state only — see the note in `eraUtils` on why the era must never
|
||||
// be inferred from individual message frames.
|
||||
export function EraBadge({ era }: EraBadgeProps) {
|
||||
return (
|
||||
<Badge variant="outline" color={isModernEra(era) ? "blue" : "gray"}>
|
||||
{formatEra(era)}
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
15
clients/web/src/components/elements/EraBadge/eraUtils.ts
Normal file
15
clients/web/src/components/elements/EraBadge/eraUtils.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import type { ProtocolEra } from "@modelcontextprotocol/client";
|
||||
|
||||
// The SDK reports an era for every connected server, including a plain legacy
|
||||
// connect (`"legacy"`); it's `undefined` only when not connected. Anything other
|
||||
// than `"modern"` is the legacy era. IMPORTANT: this reflects the *negotiated*
|
||||
// connection era — it must be fed from connection state, never inferred from
|
||||
// individual message frames (the modern probe carries a `_meta` envelope before
|
||||
// the era is known; spec §8.3).
|
||||
export function isModernEra(era: ProtocolEra | undefined): boolean {
|
||||
return era === "modern";
|
||||
}
|
||||
|
||||
export function formatEra(era: ProtocolEra | undefined): string {
|
||||
return isModernEra(era) ? "Modern" : "Legacy";
|
||||
}
|
||||
Loading…
Reference in a new issue