Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ npm run test-servers:build # (from clients/web) → tsc -p test-servers, emits

The Vite alias `@modelcontextprotocol/inspector-test-server` (in `clients/web/vite.config.ts`) points at `test-servers/build/index.js` so `getTestMcpServerPath()` resolves to a real `.js` path.

A streamable-HTTP server can also serve the **modern (2026-07-28) protocol era** via the SDK's `createMcpHandler` — set `transport.modern` in the JSON config (`true` for dual-era stateless serving, or `{ "legacy": "reject" }` for modern-only strict), or pass `modern` on the `ServerConfig` for an in-process `createTestServerHttp`. This is what lets an Inspector connection negotiating `protocolEra: "auto" | "modern"` reach the modern leg (populated `server/discover`, sessionless). See `test-servers/configs/modern-http.json`. `test-servers/configs/modern-mrtr-http.json` additionally serves the `mrtr_confirm` tool (preset `mrtr_confirm`, `createMrtrTool`) over the modern leg: its handler returns `inputRequired(...)` embedding a form elicitation, so invoking it produces a real MRTR round-trip (`input_required` → the client fulfils the embedded elicitation and retries with a new id → `complete`) — useful for eyeballing the Protocol view's MRTR conversation grouping. (The legacy `collect_elicitation` preset calls `server.elicitInput`, which errors on the 2026-07-28 leg — server→client requests aren't allowed there; MRTR is the modern replacement.)
A streamable-HTTP server can also serve the **modern (2026-07-28) protocol era** via the SDK's `createMcpHandler` — set `transport.modern` in the JSON config (`true` for dual-era stateless serving, or `{ "legacy": "reject" }` for modern-only strict), or pass `modern` on the `ServerConfig` for an in-process `createTestServerHttp`. This is what lets an Inspector connection negotiating `protocolEra: "auto" | "modern"` reach the modern leg (populated `server/discover`, sessionless). See `test-servers/configs/modern-http.json`. `test-servers/configs/modern-mrtr-http.json` additionally serves the `mrtr_confirm` tool (preset `mrtr_confirm`, `createMrtrTool`) over the modern leg: its handler returns `inputRequired(...)` embedding a form elicitation, so invoking it produces a real MRTR round-trip (`input_required` → the client fulfils the embedded elicitation and retries with a new id → `complete`). The Inspector drives MRTR manually (`inputRequired: { autoFulfill: false }`), so the embedded elicitation pauses at the pending-request modal (tagged "input_required") for you to answer, then the retry completes — useful for eyeballing both that pending-request UX and the Protocol view's MRTR conversation grouping. `test-servers/configs/mrtr-showcase-http.json` bundles every MRTR preset in one modern server for manual testing: `mrtr_confirm` (single round), `mrtr_two_step` (two elicitation rounds via `requestState`), `mrtr_sample` (embedded sampling → the Sampling panel), `mrtr_roots` (embedded `roots/list`, auto-answered silently from configured roots — no modal), `mrtr_edge` (an `inputRequests`-only round then a `requestState`-only round), and `mrtr_loop` (never completes → the `MRTR_MAX_ROUNDS` bound trips). (The legacy `collect_elicitation` preset calls `server.elicitInput`, which errors on the 2026-07-28 leg — server→client requests aren't allowed there; MRTR is the modern replacement.)

## Building

Expand Down
3 changes: 3 additions & 0 deletions clients/web/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3640,6 +3640,7 @@ function App() {
kind: "sampling",
id: activeSample.id,
request: activeSample.request.params,
origin: activeSample.origin,
};
}
const activeElicitation = pendingElicitations[0];
Expand All @@ -3651,12 +3652,14 @@ function App() {
id: activeElicitation.id,
message: params.message,
url: params.url,
origin: activeElicitation.origin,
};
}
return {
kind: "elicitation-form",
id: activeElicitation.id,
request: params,
origin: activeElicitation.origin,
};
}
return null;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { MrtrOriginNote } from "./MrtrOriginNote";

const meta: Meta<typeof MrtrOriginNote> = {
title: "Elements/MrtrOriginNote",
component: MrtrOriginNote,
};

export default meta;
type Story = StoryObj<typeof MrtrOriginNote>;

export const InputRequired: Story = {
args: { origin: "input-required" },
};

export const ServerRequest: Story = {
args: { origin: "server-request" },
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { describe, it, expect } from "vitest";
import { renderWithMantine, screen } from "../../../test/renderWithMantine";
import { MrtrOriginNote } from "./MrtrOriginNote";

describe("MrtrOriginNote", () => {
it("renders the MRTR note for a modern input-required round", () => {
renderWithMantine(<MrtrOriginNote origin="input-required" />);
expect(screen.getByText("input_required")).toBeInTheDocument();
expect(screen.getByText(/sent back as a retry/i)).toBeInTheDocument();
});

it("renders nothing for a legacy server request", () => {
renderWithMantine(<MrtrOriginNote origin="server-request" />);
expect(screen.queryByText("input_required")).not.toBeInTheDocument();
expect(screen.queryByText(/sent back as a retry/i)).not.toBeInTheDocument();
});

it("defaults to rendering nothing when origin is omitted", () => {
renderWithMantine(<MrtrOriginNote />);
expect(screen.queryByText("input_required")).not.toBeInTheDocument();
expect(screen.queryByText(/sent back as a retry/i)).not.toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { Badge, Group, Text } from "@mantine/core";
import type { PendingRequestOrigin } from "@inspector/core/mcp/types.js";

export interface MrtrOriginNoteProps {
/**
* How the pending request reached the Inspector. `"server-request"` (the
* default) is a legacy server→client request and renders nothing, keeping the
* panel visually identical to its pre-MRTR form. `"input-required"` is a
* modern (2026-07-28) MRTR round and renders the era-accurate note.
*/
origin?: PendingRequestOrigin;
}

const NoteRow = Group.withProps({
gap: "xs",
align: "center",
wrap: "nowrap",
});

const NoteText = Text.withProps({
size: "xs",
c: "dimmed",
});

/**
* Era-accurate note for a pending sampling/elicitation request. On a modern
* MRTR round (`origin: "input-required"`, SEP-2322) the request was embedded in
* a tool-call/prompt/resource `input_required` result rather than sent as a
* server→client JSON-RPC request, and the user's answer is echoed back to the
* server as a retry of the original call. Legacy requests
* (`origin: "server-request"`) render nothing so their panels are unchanged.
*/
export function MrtrOriginNote({
origin = "server-request",
}: MrtrOriginNoteProps) {
if (origin !== "input-required") return null;
return (
<NoteRow>
<Badge variant="outline" color="blue">
input_required
</Badge>
<NoteText>
The server returned input_required; your answer is sent back as a retry
of the original request (MRTR).
</NoteText>
</NoteRow>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@ import {
Text,
} from "@mantine/core";
import type { ElicitRequestFormParams } from "@modelcontextprotocol/client";
import type { PendingRequestOrigin } from "@inspector/core/mcp/types.js";
import {
hasMissingRequiredFields,
type InspectorFormSchema,
} from "../../../utils/jsonUtils";
import { SchemaForm } from "../SchemaForm/SchemaForm";
import { MrtrOriginNote } from "../../elements/MrtrOriginNote/MrtrOriginNote";

export interface ElicitationFormPanelProps {
request: ElicitRequestFormParams;
Expand All @@ -24,6 +26,12 @@ export interface ElicitationFormPanelProps {
onDecline: () => void;
/** Dismissal without an explicit choice (maps to the spec's `cancel`). */
onCancel: () => void;
/**
* How the request reached the Inspector — a legacy server→client request or a
* modern MRTR `input_required` round. Drives the era-accurate note; defaults
* to `"server-request"` (legacy, note hidden). (#1704)
*/
origin?: PendingRequestOrigin;
/**
* A response has been dispatched; lock the actions so a second click can't
* resolve the request twice (the underlying handler throws if called again).
Expand Down Expand Up @@ -66,6 +74,7 @@ export function ElicitationFormPanel({
onSubmit,
onDecline,
onCancel,
origin = "server-request",
busy = false,
}: ElicitationFormPanelProps) {
const requestedSchema = request.requestedSchema as InspectorFormSchema;
Expand All @@ -74,6 +83,7 @@ export function ElicitationFormPanel({
return (
<Stack gap="md">
<QuotedMessage>{formatQuoted(request.message)}</QuotedMessage>
<MrtrOriginNote origin={origin} />
<Divider />
<FieldScroll>
<SchemaForm
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,19 @@ import {
Stack,
Text,
} from "@mantine/core";
import type { PendingRequestOrigin } from "@inspector/core/mcp/types.js";
import { MrtrOriginNote } from "../../elements/MrtrOriginNote/MrtrOriginNote";

export interface ElicitationUrlPanelProps {
message: string;
url: string;
requestId: string;
/**
* How the request reached the Inspector — a legacy server→client request or a
* modern MRTR `input_required` round. Drives the era-accurate note; defaults
* to `"server-request"` (legacy, note hidden). (#1704)
*/
origin?: PendingRequestOrigin;
/**
* The URL has been opened and we're waiting for the user to confirm they
* finished the external flow. Reveals the "I've completed it" action; opening
Expand Down Expand Up @@ -65,11 +73,13 @@ export function ElicitationUrlPanel({
onOpenInBrowser,
onComplete,
onCancel,
origin = "server-request",
busy = false,
}: ElicitationUrlPanelProps) {
return (
<Stack gap="md">
<ItalicMessage>{message}</ItalicMessage>
<MrtrOriginNote origin={origin} />
<Divider />
<Text size="sm">The server is requesting you visit:</Text>
<WrappingCode>{url}</WrappingCode>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,21 @@ describe("InlineElicitationRequest", () => {
expect(screen.getByText("elicitation/create (form)")).toBeInTheDocument();
expect(screen.getByText(formRequest.message)).toBeInTheDocument();
expect(screen.getByText("1 of 1")).toBeInTheDocument();
// Default origin is a legacy server request — no MRTR note.
expect(screen.queryByText("input_required")).not.toBeInTheDocument();
});

it("shows the MRTR note for a modern input_required round", () => {
renderWithMantine(
<InlineElicitationRequest
{...baseProps}
request={formRequest}
values={{}}
origin="input-required"
/>,
);
expect(screen.getByText("input_required")).toBeInTheDocument();
expect(screen.getByText(/sent back as a retry/i)).toBeInTheDocument();
});

it("renders schema fields and a Submit button in form mode", () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,22 @@ import type {
ElicitRequest,
ElicitRequestFormParams,
} from "@modelcontextprotocol/client";
import type { PendingRequestOrigin } from "@inspector/core/mcp/types.js";
import type { InspectorFormSchema } from "../../../utils/jsonUtils";
import { SchemaForm } from "../SchemaForm/SchemaForm";
import { MrtrOriginNote } from "../../elements/MrtrOriginNote/MrtrOriginNote";

export interface InlineElicitationRequestProps {
request: ElicitRequest["params"];
queuePosition: string;
values?: Record<string, unknown>;
isWaiting?: boolean;
/**
* How the request reached the Inspector — a legacy server→client request or a
* modern MRTR `input_required` round. Drives the era-accurate note; defaults
* to `"server-request"` (legacy, note hidden). (#1704)
*/
origin?: PendingRequestOrigin;
onChange: (values: Record<string, unknown>) => void;
onSubmit: () => void;
onCancel: () => void;
Expand Down Expand Up @@ -73,6 +81,7 @@ export function InlineElicitationRequest({
queuePosition,
values,
isWaiting,
origin = "server-request",
onChange,
onSubmit,
onCancel,
Expand All @@ -86,6 +95,7 @@ export function InlineElicitationRequest({
</Group>

<ItalicMessage>{request.message}</ItalicMessage>
<MrtrOriginNote origin={origin} />

{isFormMode(request) && (
<SchemaForm
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,20 @@ describe("InlineSamplingRequest", () => {
expect(screen.getByText("sampling/createMessage")).toBeInTheDocument();
expect(screen.getByText("1 of 1")).toBeInTheDocument();
expect(screen.getByText("Please analyze this code.")).toBeInTheDocument();
// Default origin is a legacy server request — no MRTR note.
expect(screen.queryByText("input_required")).not.toBeInTheDocument();
});

it("shows the MRTR note for a modern input_required round", () => {
renderWithMantine(
<InlineSamplingRequest
{...baseProps}
request={textRequest}
origin="input-required"
/>,
);
expect(screen.getByText("input_required")).toBeInTheDocument();
expect(screen.getByText(/sent back as a retry/i)).toBeInTheDocument();
});

it("renders model hints when provided", () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,19 @@ import type {
CreateMessageRequestParams,
CreateMessageResult,
} from "@modelcontextprotocol/client";
import type { PendingRequestOrigin } from "@inspector/core/mcp/types.js";
import { MrtrOriginNote } from "../../elements/MrtrOriginNote/MrtrOriginNote";

export interface InlineSamplingRequestProps {
request: CreateMessageRequestParams;
queuePosition: string;
draftResult?: CreateMessageResult;
/**
* How the request reached the Inspector — a legacy server→client request or a
* modern MRTR `input_required` round. Drives the era-accurate note; defaults
* to `"server-request"` (legacy, note hidden). (#1704)
*/
origin?: PendingRequestOrigin;
onAutoRespond: () => void;
onEditAndSend: () => void;
onReject: () => void;
Expand Down Expand Up @@ -94,6 +102,7 @@ export function InlineSamplingRequest({
request,
queuePosition,
draftResult,
origin = "server-request",
onAutoRespond,
onEditAndSend,
onReject,
Expand All @@ -111,6 +120,8 @@ export function InlineSamplingRequest({
<QueueLabel>{queuePosition}</QueueLabel>
</Group>

<MrtrOriginNote origin={origin} />

{modelHints && <Text size="sm">{formatModelHints(modelHints)}</Text>}

<PreviewText>{messagePreview}</PreviewText>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,12 @@ type Story = StoryObj<typeof PendingClientRequestModal>;

export const Sampling: Story = {
args: {
request: { kind: "sampling", id: "sampling-1", request: samplingRequest },
request: {
kind: "sampling",
id: "sampling-1",
request: samplingRequest,
origin: "server-request",
},
},
play: async ({ canvasElement, args }) => {
const body = within(canvasElement.ownerDocument.body);
Expand All @@ -125,12 +130,32 @@ export const Sampling: Story = {
},
};

// A modern MRTR round: the sampling request was embedded in a tool-call
// `input_required` result, so the panel shows the era-accurate note.
export const SamplingInputRequired: Story = {
args: {
request: {
kind: "sampling",
id: "sampling-mrtr",
request: samplingRequest,
origin: "input-required",
},
},
play: async ({ canvasElement }) => {
const body = within(canvasElement.ownerDocument.body);
await body.findByText("Sampling Request");
expect(body.getByText("input_required")).toBeInTheDocument();
expect(body.getByText(/sent back as a retry/i)).toBeInTheDocument();
},
};

export const SamplingTall: Story = {
args: {
request: {
kind: "sampling",
id: "sampling-tall",
request: tallSamplingRequest,
origin: "server-request",
},
},
};
Expand All @@ -141,6 +166,7 @@ export const ElicitationForm: Story = {
kind: "elicitation-form",
id: "elicitation-1",
request: formRequest,
origin: "server-request",
},
},
play: async ({ canvasElement, args }) => {
Expand All @@ -163,6 +189,7 @@ export const ElicitationFormTall: Story = {
kind: "elicitation-form",
id: "elicitation-tall",
request: tallFormRequest,
origin: "server-request",
},
},
};
Expand All @@ -174,6 +201,7 @@ export const ElicitationUrl: Story = {
id: "elicitation-2",
message: "Authorize access in your browser to continue.",
url: "https://example.com/authorize?token=abc123",
origin: "server-request",
},
},
play: async ({ canvasElement }) => {
Expand Down
Loading
Loading