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
12
test-servers/configs/modern-mrtr-http.json
Normal file
12
test-servers/configs/modern-mrtr-http.json
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"serverInfo": {
|
||||
"name": "composable-modern-mrtr",
|
||||
"version": "1.0.0"
|
||||
},
|
||||
"tools": [{ "preset": "echo" }, { "preset": "mrtr_confirm" }],
|
||||
"transport": {
|
||||
"type": "streamable-http",
|
||||
"port": 3101,
|
||||
"modern": true
|
||||
}
|
||||
}
|
||||
|
|
@ -11,6 +11,7 @@ import {
|
|||
ResourceTemplate as SdkResourceTemplate,
|
||||
completable,
|
||||
isCompletable,
|
||||
isInputRequiredResult,
|
||||
} from "@modelcontextprotocol/server";
|
||||
import type {
|
||||
Implementation,
|
||||
|
|
@ -19,6 +20,7 @@ import type {
|
|||
ResourceTemplateType as ResourceTemplate,
|
||||
Prompt,
|
||||
CallToolResult,
|
||||
InputRequiredResult,
|
||||
PromptArgument,
|
||||
RegisteredTool,
|
||||
RegisteredResource,
|
||||
|
|
@ -80,6 +82,16 @@ export interface HandlerExtra {
|
|||
method: string;
|
||||
params?: Record<string, unknown>;
|
||||
}) => Promise<void>;
|
||||
/**
|
||||
* MRTR (2026-07-28): the bare input responses a retried request echoes back,
|
||||
* keyed by the identifiers the server assigned in `inputRequests`. Present
|
||||
* only on the retry round, so an MRTR tool branches on it to distinguish the
|
||||
* original call (return `inputRequired(...)`) from the retry (return the final
|
||||
* result). Undefined on legacy connections and on the first round.
|
||||
*/
|
||||
inputResponses?: Record<string, unknown>;
|
||||
/** MRTR: the opaque `requestState` the client echoed on retry (resolved value). */
|
||||
requestState?: unknown;
|
||||
}
|
||||
|
||||
/** Minimal structural view of `ctx.mcpReq` for building {@link HandlerExtra}. */
|
||||
|
|
@ -90,6 +102,10 @@ interface McpReqContext {
|
|||
signal?: AbortSignal;
|
||||
send?: HandlerExtra["sendRequest"];
|
||||
notify?: HandlerExtra["sendNotification"];
|
||||
/** MRTR input responses on a retried request (2026-07-28). */
|
||||
inputResponses?: Record<string, unknown>;
|
||||
/** MRTR request-state accessor (resolves the echoed opaque token). */
|
||||
requestState?: () => unknown;
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -102,6 +118,8 @@ function toHandlerExtra(ctx: ServerContext | undefined): HandlerExtra {
|
|||
signal: mcpReq?.signal,
|
||||
sendRequest: mcpReq?.send,
|
||||
sendNotification: mcpReq?.notify,
|
||||
inputResponses: mcpReq?.inputResponses,
|
||||
requestState: mcpReq?.requestState?.(),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -296,7 +314,10 @@ export interface ToolDefinition {
|
|||
params: Record<string, unknown>,
|
||||
context?: TestServerContext,
|
||||
extra?: HandlerExtra,
|
||||
) => Promise<CallToolResult>;
|
||||
// A handler may return an `InputRequiredResult` (via `inputRequired(...)`)
|
||||
// on the 2026-07-28 leg to drive an MRTR round-trip; the wrapper forwards it
|
||||
// verbatim rather than coercing it to a `CallToolResult`.
|
||||
) => Promise<CallToolResult | InputRequiredResult>;
|
||||
}
|
||||
|
||||
export interface TaskToolDefinition {
|
||||
|
|
@ -745,6 +766,14 @@ export function createMcpServer(config: ServerConfig): McpServer {
|
|||
context,
|
||||
toHandlerExtra(ctx),
|
||||
);
|
||||
// MRTR (2026-07-28): a handler may return an `input_required` result
|
||||
// to request input before completing. Forward it verbatim — the SDK
|
||||
// serves it as the modern input_required result and the client
|
||||
// retries. (Coercing it into a CallToolResult below would strip
|
||||
// `resultType`/`inputRequests`/`requestState`.)
|
||||
if (isInputRequiredResult(result)) {
|
||||
return result;
|
||||
}
|
||||
const rawStructured =
|
||||
result &&
|
||||
typeof result === "object" &&
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import {
|
|||
createCollectSampleTool,
|
||||
createListRootsTool,
|
||||
createCollectFormElicitationTool,
|
||||
createMrtrTool,
|
||||
createCollectUrlElicitationTool,
|
||||
createUrlElicitationFormTool,
|
||||
createSendNotificationTool,
|
||||
|
|
@ -96,6 +97,8 @@ function resolveToolPreset(
|
|||
return createListRootsTool();
|
||||
case "collect_elicitation":
|
||||
return createCollectFormElicitationTool();
|
||||
case "mrtr_confirm":
|
||||
return createMrtrTool();
|
||||
case "collect_url_elicitation":
|
||||
return createCollectUrlElicitationTool();
|
||||
case "url_elicitation_form":
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in a new issue