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

* 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:
Cliff Hall 2026-07-17 20:02:11 -04:00 • committed by GitHub
commit 73d9b181fe
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -10,7 +10,7 @@ import {
Stack,
Text,
} from "@mantine/core";
import type { MessageEntry } from "../../../../../../core/mcp/types.js";
import type { MessageEntry } from "@inspector/core/mcp/types.js";
import { ContentViewer } from "../../elements/ContentViewer/ContentViewer";
import { CopyButton } from "../../elements/CopyButton/CopyButton";
import { MessageDirectionBadge } from "../../elements/MessageDirectionBadge/MessageDirectionBadge";
@ -18,7 +18,13 @@ import { MethodBadge } from "../../elements/MethodBadge/MethodBadge";
import { ExpandToggle } from "../../elements/ExpandToggle/ExpandToggle";
import { PinToggle } from "../../elements/PinToggle/PinToggle";
import { ReplayButton } from "../../elements/ReplayButton/ReplayButton";
import { extractMethod, isReplayableProtocolMethod } from "../protocolUtils.js";
import {
extractMethod,
extractResultType,
extractSubscriptionId,
isModernFrame,
isReplayableProtocolMethod,
} from "../protocolUtils.js";
export interface ProtocolEntryProps {
entry: MessageEntry;
@ -83,6 +89,46 @@ const DurationText = Text.withProps({
c: "dimmed",
});
// Flags a frame whose method only exists in the modern (2026-07-28) era
// (`server/discover`, `subscriptions/listen`, its acknowledgement). This labels
// the *frame*, not the connection's negotiated era (see the transcript caveat in
// protocolUtils) — those methods genuinely cannot occur on a legacy connection.
const ModernFrameBadge = Badge.withProps({
color: "blue",
variant: "outline",
});
// The `subscriptionId` tag on a modern push notification (spec §7.4). Shown with
// a copy button so the id can be correlated against the `subscriptions/listen`
// stream that opened it.
const SubscriptionLabel = Text.withProps({
size: "sm",
c: "dimmed",
});
const SubscriptionId = Text.withProps({
size: "sm",
ff: "monospace",
});
const SubscriptionCluster = Group.withProps({
gap: 4,
wrap: "nowrap",
miw: 0,
});
// `complete` is green — it's the success signal now that the redundant "OK"
// status badge is suppressed, so a modern success keeps the same at-a-glance
// green affordance a legacy success has. `input_required` is yellow (in
// progress: awaiting input before the retry).
function resultTypeColor(resultType: "complete" | "input_required"): string {
return resultType === "input_required" ? "yellow" : "green";
}
function resultTypeLabel(resultType: "complete" | "input_required"): string {
return resultType === "input_required" ? "input required" : "complete";
}
const SubtleButton = Button.withProps({
variant: "subtle",
size: "xs",
@ -164,6 +210,9 @@ export function ProtocolEntry({
const resourceUri = extractResourceUri(entry);
const status = extractStatus(entry);
const canReplay = isReplayableProtocolMethod(method);
const resultType = extractResultType(entry);
const subscriptionId = extractSubscriptionId(entry);
const isModern = isModernFrame(method);
useEffect(() => {
setIsExpanded(isListExpanded);
@ -174,11 +223,33 @@ export function ProtocolEntry({
direction={entry.origin === "client" ? "outgoing" : "incoming"}
/>
);
const statusBadge = status !== "none" && (
// The modern `resultType` on the paired result (spec §7.3): `input_required`
// (the operation isn't done — it needs input and will be retried) vs the
// ordinary `complete`. Only present on modern results, so it doubles as a
// per-result modern signal without inferring the connection era.
const resultTypeBadge = resultType && (
<Badge color={resultTypeColor(resultType)} variant="status">
{resultTypeLabel(resultType)}
</Badge>
);
// Suppress the redundant green "OK" when a `resultType` badge already conveys
// the outcome (a modern success is `complete`/`input required`); errors and
// pending have no `resultType`, so their status badge still shows.
const statusBadge = status !== "none" && !resultType && (
<Badge color={statusColor(status)} variant="status">
{statusLabel(status)}
</Badge>
);
const modernFrameBadge = isModern && (
<ModernFrameBadge>modern</ModernFrameBadge>
);
const subscriptionBadge = subscriptionId && (
<SubscriptionCluster>
<SubscriptionLabel>sub</SubscriptionLabel>
<CopyButton value={subscriptionId} />
<SubscriptionId>{subscriptionId}</SubscriptionId>
</SubscriptionCluster>
);
const durationText = entry.duration != null && (
<DurationText>{formatDuration(entry.duration)}</DurationText>
);
@ -198,12 +269,15 @@ export function ProtocolEntry({
</HeaderCluster>
<ControlsCluster>
{durationText}
{resultTypeBadge}
{statusBadge}
</ControlsCluster>
</HeaderRow>
<HeaderRow>
<HeaderCluster flex={1}>
<MethodBadge method={method} />
{modernFrameBadge}
{subscriptionBadge}
{target && (
<>
{resourceUri && <CopyButton value={resourceUri} />}
@ -241,6 +315,8 @@ export function ProtocolEntry({
</TimestampText>
{directionBadge}
<MethodBadge method={method} />
{modernFrameBadge}
{subscriptionBadge}
{target && (
<>
{resourceUri && <CopyButton value={resourceUri} />}
@ -250,6 +326,7 @@ export function ProtocolEntry({
</Group>
<Group gap="sm">
{durationText}
{resultTypeBadge}
{statusBadge}
</Group>
</HeaderRow>