Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
15 changes: 14 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ Each config below is a ready-made server for exercising one feature by hand. Loa
| ----------------------------------------- | -------------------------------------------------- | ---------------------------------------------------------------------- |
| `mcp-app-http.json` **(legacy era)** | An MCP App (UI resource + app tool) in the Apps tab | [#1859](https://github.com/modelcontextprotocol/inspector/issues/1859) |
| `modern-mrtr-http.json` | A single MRTR round-trip | — |
| `mrtr-showcase-http.json` | Every MRTR preset in one server | — |
| `mrtr-showcase-http.json` | Every MRTR preset in one server | [#1860](https://github.com/modelcontextprotocol/inspector/issues/1860) |
| `modern-network-http.json` | Network tab: `Mcp-*` headers + error taxonomy | [#1628](https://github.com/modelcontextprotocol/inspector/issues/1628) |
| `xmcpheader-modern-http.json` | Tools tab: `x-mcp-header` mirroring and exclusions | [#1632](https://github.com/modelcontextprotocol/inspector/issues/1632) |
| `pagination-http.json` | Page-by-page list fetching | [#1721](https://github.com/modelcontextprotocol/inspector/issues/1721) |
Expand Down Expand Up @@ -175,8 +175,21 @@ The Inspector drives MRTR manually (`inputRequired: { autoFulfill: false }`), so
| `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 |
| `mrtr_empty` | Completes with an empty result — no `content`, no `structuredContent` |
| `mrtr_loop` | Never completes → trips the `MRTR_MAX_ROUNDS` bound |

Run `mrtr_empty` and answer its single elicitation: the Protocol tab groups the
exchange as an MRTR conversation ending **COMPLETE**, and the Results panel says
**"Empty result — The tool call completed successfully and returned no content."**
On the broken build that same result rendered as **"No results yet"**, the panel's
pre-run placeholder ([#1860](https://github.com/modelcontextprotocol/inspector/issues/1860)) —
so a call the user had just watched succeed read as a call that never ran. An
empty `content` array with no `structuredContent` is a legal `CallToolResult`,
and the panel only ever mounts once a result exists, so the placeholder wording
could not be true there. (The neighbouring half of the same gap — a result whose
payload lives only in `structuredContent` — was closed by
[#1908](https://github.com/modelcontextprotocol/inspector/issues/1908).)

> 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.

#### Network tab — standardized headers and error taxonomy
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,20 @@ describe("ToolResultPanel", () => {
expect(screen.getByText("boom")).toBeInTheDocument();
});

it("renders the empty state when content is empty", () => {
// #1860: this panel only mounts once a result exists, so an empty result must
// read as "the call completed and returned nothing" — never as the pre-run
// "No results yet" placeholder, which denies a call the user watched succeed.
it("names the outcome when a completed result has no content", () => {
renderWithMantine(
<ToolResultPanel result={emptyResult} onClear={() => {}} />,
);
expect(screen.getByText("No results yet")).toBeInTheDocument();
expect(screen.getByText("Empty result")).toBeInTheDocument();
expect(
screen.getByText(
"The tool call completed successfully and returned no content.",
),
).toBeInTheDocument();
expect(screen.queryByText("No results yet")).not.toBeInTheDocument();
});

it("groups resource_link blocks in a scrollable Resource Links box", async () => {
Expand Down Expand Up @@ -141,7 +150,7 @@ describe("ToolResultPanel", () => {
onClear={() => {}}
/>,
);
expect(screen.queryByText("No results yet")).not.toBeInTheDocument();
expect(screen.queryByText("Empty result")).not.toBeInTheDocument();
expect(
screen.getByRole("heading", { name: "Structured Output" }),
).toBeInTheDocument();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import {
Paper,
ScrollArea,
Stack,
Text,
Title,
} from "@mantine/core";
import type {
Expand Down Expand Up @@ -176,6 +175,16 @@ const ErrorAlert = Alert.withProps({
title: "Tool Error",
});

// A completed call whose result renders nothing. `content: []` with no
// `structuredContent` is a legal CallToolResult, and this panel is only mounted
// once a result exists — so saying "no results yet" here would deny a call the
// user just watched succeed (#1860). Name the outcome instead.
const EmptyResultAlert = Alert.withProps({
color: "gray",
variant: "light",
title: "Empty result",
});

function ResourceLinksGroup({
links,
onReadResource,
Expand Down Expand Up @@ -277,7 +286,9 @@ export function ToolResultPanel({
</ResultScroll>
) : result.content.length === 0 && !structuredNode ? (
<ResultScroll>
<Text c="dimmed">No results yet</Text>
<EmptyResultAlert>
The tool call completed successfully and returned no content.
</EmptyResultAlert>
</ResultScroll>
) : hasLinks ? (
<FillStack>
Expand Down
1 change: 1 addition & 0 deletions test-servers/configs/mrtr-showcase-http.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
{ "preset": "mrtr_sample" },
{ "preset": "mrtr_roots" },
{ "preset": "mrtr_edge" },
{ "preset": "mrtr_empty" },
{ "preset": "mrtr_loop" }
],
"transport": {
Expand Down
3 changes: 3 additions & 0 deletions test-servers/src/preset-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
createMrtrSamplingTool,
createMrtrLoopTool,
createMrtrEdgeCaseTool,
createMrtrEmptyResultTool,
createCollectUrlElicitationTool,
createUrlElicitationFormTool,
createSendNotificationTool,
Expand Down Expand Up @@ -151,6 +152,8 @@ function resolveToolPreset(
return createMrtrLoopTool();
case "mrtr_edge":
return createMrtrEdgeCaseTool();
case "mrtr_empty":
return createMrtrEmptyResultTool();
case "collect_url_elicitation":
return createCollectUrlElicitationTool();
case "url_elicitation_form":
Expand Down
44 changes: 44 additions & 0 deletions test-servers/src/test-server-fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -755,6 +755,50 @@ export function createMrtrEdgeCaseTool(): ToolDefinition {
};
}

/**
* An MRTR tool whose FINAL (complete) result carries an empty `content` array
* and no `structuredContent` — a legal `CallToolResult` that renders nothing.
*
* This is the shape behind #1860: the sequence completes (the Protocol tab shows
* the conversation COMPLETE), a real result is stored, and the Results panel had
* no way to say so — it fell through to the same "No results yet" placeholder it
* shows before anything has run, so a successful call read as a call that never
* happened. Kept as an MRTR tool rather than a plain one because the multi-round
* case is where the ambiguity actually misleads: the user has answered prompts
* and watched rounds complete, so "no results yet" is flatly contradicted by
* what they just did.
*/
export function createMrtrEmptyResultTool(): ToolDefinition {
Comment thread
cliffhall marked this conversation as resolved.
return {
name: "mrtr_empty",
description:
"MRTR tool that completes with an empty result (no content, no structuredContent).",
inputSchema: {},
handler: async (
_params: Record<string, unknown>,
_context?: TestServerContext,
extra?: HandlerExtra,
) => {
if (extra?.inputResponses?.ack === undefined) {
return inputRequired({
inputRequests: {
ack: inputRequired.elicit({
message: "Acknowledge to finish (the result will be empty)",
requestedSchema: {
type: "object",
properties: { ack: { type: "boolean", title: "Acknowledge" } },
required: ["ack"],
},
}),
},
requestState: `mrtr-empty:${++mrtrMintCount}`,
});
}
return { content: [] } satisfies CallToolResult;
},
};
}

/**
* 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
Expand Down