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>
169 lines
6.6 KiB
TypeScript
169 lines
6.6 KiB
TypeScript
import type { MessageEntry, MessageMethod } from "@inspector/core/mcp/types.js";
|
||
import {
|
||
isInputRequiredResult,
|
||
SUBSCRIPTION_ID_META_KEY,
|
||
} from "@modelcontextprotocol/client";
|
||
|
||
export function extractMethod(entry: MessageEntry): MessageMethod {
|
||
if ("method" in entry.message) {
|
||
// Cast: SDK types message.method as `string`, but every entry in this
|
||
// app's MessageEntry log originates from MCP SDK schemas.
|
||
return entry.message.method as MessageMethod;
|
||
}
|
||
return "response";
|
||
}
|
||
|
||
/**
|
||
* Request methods the Protocol Replay action can re-issue (client→server reads
|
||
* and calls). Server→client requests (roots/list, sampling, elicitation) and
|
||
* side-effectful methods (logging/setLevel, subscribe) are intentionally
|
||
* excluded. Single source of truth: `ProtocolEntry` hides the Replay button for
|
||
* anything not listed here, and App's `replayProtocolRequest` gates dispatch on
|
||
* the same set.
|
||
*/
|
||
export const REPLAYABLE_PROTOCOL_METHODS: ReadonlySet<string> = new Set([
|
||
"tools/call",
|
||
"prompts/get",
|
||
"resources/read",
|
||
"tools/list",
|
||
"prompts/list",
|
||
"resources/list",
|
||
"resources/templates/list",
|
||
"tasks/list",
|
||
"ping",
|
||
]);
|
||
|
||
export function isReplayableProtocolMethod(method: string): boolean {
|
||
return REPLAYABLE_PROTOCOL_METHODS.has(method);
|
||
}
|
||
|
||
// --- Modern-era (2026-07-28) message vocabulary -----------------------------
|
||
//
|
||
// The modern era changes the over-the-wire conversation (spec §7.2–7.4): every
|
||
// result carries a `resultType`, server→client interactions become MRTR (the
|
||
// server *returns* `input_required` and the client *retries* with a new id),
|
||
// and push notifications move to a `subscriptions/listen` stream. These helpers
|
||
// read that vocabulary off the transport-level `MessageEntry` log so the
|
||
// Protocol view can render and correlate it. IMPORTANT: they classify individual
|
||
// *frames* — they must NOT be used to decide a connection's era. The modern
|
||
// probe carries the `_meta` envelope before the era is negotiated, so "saw an
|
||
// envelope/resultType" ≠ "modern negotiated" (spec §8.3). Era labeling comes
|
||
// from the negotiated `protocolEra` connection state, threaded in as a prop.
|
||
|
||
/**
|
||
* Frame methods that only exist in the modern era: the discovery probe, the
|
||
* push-notification listen stream, and its acknowledgement. Generic viewers
|
||
* already display them; this set drives the small "modern frame" affordance.
|
||
*/
|
||
export const MODERN_METHODS: ReadonlySet<string> = new Set([
|
||
"server/discover",
|
||
"subscriptions/listen",
|
||
"notifications/subscriptions/acknowledged",
|
||
]);
|
||
|
||
export function isModernFrame(method: string): boolean {
|
||
return MODERN_METHODS.has(method);
|
||
}
|
||
|
||
// The successful `result` object of a request entry's paired response, or
|
||
// undefined when there is no response or it was an error. (messageLogState folds
|
||
// a request's response onto the request entry by JSON-RPC id.)
|
||
function getResponseResult(
|
||
entry: MessageEntry,
|
||
): Record<string, unknown> | undefined {
|
||
const response = entry.response;
|
||
if (!response || "error" in response) return undefined;
|
||
const result = response.result;
|
||
return result && typeof result === "object"
|
||
? (result as Record<string, unknown>)
|
||
: undefined;
|
||
}
|
||
|
||
function getMessageParams(
|
||
entry: MessageEntry,
|
||
): Record<string, unknown> | undefined {
|
||
const msg = entry.message;
|
||
if (!("params" in msg) || !msg.params) return undefined;
|
||
return msg.params as Record<string, unknown>;
|
||
}
|
||
|
||
/**
|
||
* The modern `resultType` discriminator on a request's paired result:
|
||
* `"input_required"` (the server needs input before it can complete) or
|
||
* `"complete"`. Undefined for legacy results (no `resultType` on the wire),
|
||
* errors, notifications, and pending requests — so a `resultType` badge only
|
||
* shows where the modern era actually put one.
|
||
*/
|
||
export function extractResultType(
|
||
entry: MessageEntry,
|
||
): "complete" | "input_required" | undefined {
|
||
const result = getResponseResult(entry);
|
||
if (!result) return undefined;
|
||
if (isInputRequiredResult(result)) return "input_required";
|
||
return result.resultType === "complete" ? "complete" : undefined;
|
||
}
|
||
|
||
/**
|
||
* The opaque MRTR `requestState` token that links the rounds of one logical
|
||
* operation across multiple JSON-RPC ids. It appears on the `input_required`
|
||
* *result* (original call) and is echoed back in the *params* of the retried
|
||
* request (spec §7.3). Returns undefined for non-MRTR traffic.
|
||
*/
|
||
export function extractRequestState(entry: MessageEntry): string | undefined {
|
||
const result = getResponseResult(entry);
|
||
const fromResult = result?.requestState;
|
||
if (typeof fromResult === "string" && fromResult.length > 0)
|
||
return fromResult;
|
||
const params = getMessageParams(entry);
|
||
const fromParams = params?.requestState;
|
||
if (typeof fromParams === "string" && fromParams.length > 0)
|
||
return fromParams;
|
||
return undefined;
|
||
}
|
||
|
||
/**
|
||
* The `subscriptionId` a modern push notification is tagged with, carried in
|
||
* `params._meta` under `io.modelcontextprotocol/subscriptionId` (spec §7.4).
|
||
* Undefined for untagged frames.
|
||
*/
|
||
export function extractSubscriptionId(entry: MessageEntry): string | undefined {
|
||
const params = getMessageParams(entry);
|
||
const meta = params?._meta as Record<string, unknown> | undefined;
|
||
const id = meta?.[SUBSCRIPTION_ID_META_KEY];
|
||
return typeof id === "string" ? id : undefined;
|
||
}
|
||
|
||
/**
|
||
* A rendered row in the Protocol list: either a single message entry, or an
|
||
* MRTR conversation — the contiguous run of entries sharing one `requestState`
|
||
* (original call → `input_required` → retried call → final result), grouped so
|
||
* one logical operation renders as one expandable unit.
|
||
*/
|
||
export type ProtocolRow =
|
||
| { kind: "single"; entry: MessageEntry }
|
||
| { kind: "mrtr"; requestState: string; rounds: MessageEntry[] };
|
||
|
||
/**
|
||
* Fold a (already filtered/sorted) entry list into rows, clustering contiguous
|
||
* entries that share a non-empty `requestState` into one MRTR row. Contiguity is
|
||
* safe because the SDK auto-fulfils MRTR input in-process (no intervening wire
|
||
* frames) so an operation's rounds are adjacent in the log. Order is preserved;
|
||
* everything without a `requestState` stays a `single` row.
|
||
*/
|
||
export function groupProtocolEntries(entries: MessageEntry[]): ProtocolRow[] {
|
||
const rows: ProtocolRow[] = [];
|
||
for (const entry of entries) {
|
||
const requestState = extractRequestState(entry);
|
||
if (requestState) {
|
||
const last = rows[rows.length - 1];
|
||
if (last?.kind === "mrtr" && last.requestState === requestState) {
|
||
last.rounds.push(entry);
|
||
continue;
|
||
}
|
||
rows.push({ kind: "mrtr", requestState, rounds: [entry] });
|
||
continue;
|
||
}
|
||
rows.push({ kind: "single", entry });
|
||
}
|
||
return rows;
|
||
}
|