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

@ -8,6 +8,7 @@
import * as z from "zod/v4";
import {
RELATED_TASK_META_KEY,
inputRequired,
type Implementation,
type ElicitRequestFormParams,
type ElicitRequestURLParams,
@ -368,6 +369,79 @@ export function createCollectFormElicitationTool(): ToolDefinition {
};
}
/**
* Create an "mrtr_confirm" tool exercising the modern (2026-07-28) multi
* round-trip request (MRTR) flow. On the first call it returns an
* `input_required` result embedding a form elicitation ("Confirm: <action>?");
* the client fulfils it and retries with `inputResponses`, on which the handler
* returns the final result. This is the modern replacement for a server→client
* `elicitation/create` request — `createCollectFormElicitationTool` (which calls
* `server.elicitInput`) is legacy-only and errors on the 2026-07-28 leg.
*
* Used by `test-servers/configs/modern-mrtr-http.json` so the Inspector's
* History view can render a real MRTR round-trip as one grouped conversation.
*/
// Process-wide counter so each original MRTR call mints a DISTINCT
// `requestState`. It must live at MODULE scope, not inside `createMrtrTool` or
// its handler closure: the modern (2026-07-28) leg is stateless — the SDK's
// `createMcpHandler` rebuilds the server (and every tool closure) per request
// (see `test-server-http.ts` `startModernHttp`), so a per-closure counter would
// reset to 0 on every request and every token would end in `:1`, defeating the
// point. A stable-per-action token would let two same-action calls (whose rounds
// land adjacently in the History log) fold into a single `MrtrConversation`,
// since grouping clusters contiguous entries sharing a token — confusing for a
// demo whose whole point is eyeballing the grouping. Only bumped on the mint
// (first) round; the retry echoes the token, it isn't re-minted. Real SDK tokens
// are already unique per operation.
let mrtrMintCount = 0;
export function createMrtrTool(): ToolDefinition {
return {
name: "mrtr_confirm",
description:
"Multi round-trip tool: asks the client to confirm an action via an embedded elicitation, then completes.",
inputSchema: {
action: z
.string()
.describe("The action to confirm before the tool completes"),
},
handler: async (
params: Record<string, unknown>,
_context?: TestServerContext,
extra?: HandlerExtra,
) => {
const action =
typeof params.action === "string" ? params.action : "the action";
const responses = extra?.inputResponses;
// First round: no answers yet — return input_required embedding a form
// elicitation and an opaque requestState the client echoes on retry.
if (!responses || responses.confirm === undefined) {
return inputRequired({
inputRequests: {
confirm: inputRequired.elicit({
message: `Confirm: ${action}?`,
requestedSchema: {
type: "object",
properties: {
confirm: { type: "boolean", title: "Confirm" },
},
required: ["confirm"],
},
}),
},
requestState: `mrtr:${action}:${++mrtrMintCount}`,
});
}
// Retry round: the client fulfilled the elicitation and echoed the answer.
return toToolResult(
`MRTR complete — confirmation for "${action}": ${JSON.stringify(responses.confirm)}`,
);
},
};
}
/**
* Create a "url_elicitation_form" tool that spins up a simple HTTP server on a dynamic
* port with a form page, sends that URL via URL elicitation, and on form submit collects
@ -1217,8 +1291,7 @@ export function createAddToolTool(): ToolDefinition {
{
description: params.description as string,
inputSchema: params.inputSchema as
| Record<string, z.ZodType>
| undefined,
Record<string, z.ZodType> | undefined,
},
async () => {
return {
@ -1322,8 +1395,7 @@ export function createAddPromptTool(): ToolDefinition {
{
description: params.description as string | undefined,
argsSchema: params.argsSchema as
| Record<string, z.ZodType>
| undefined,
Record<string, z.ZodType> | undefined,
},
async () => {
return {
@ -1467,9 +1539,7 @@ export function createSendProgressTool(
// Extract progressToken from metadata
const progressToken = extra?._meta?.progressToken as
| string
| number
| undefined;
string | number | undefined;
// Send progress notifications
let sent = 0;
@ -1905,12 +1975,9 @@ export function createTaskTool(
handler: {
createTask: async (args, extra) => {
const message = (args as Record<string, unknown>)?.message as
| string
| undefined;
string | undefined;
const progressToken = extra._meta?.progressToken as
| string
| number
| undefined;
string | number | undefined;
const task = await extra.taskStore.createTask({});
runTaskExecution({
task,