Merge pull request #2034 from modelcontextprotocol/v2/fix/1860-mrtr-results-panel
fix(web): name an empty tool result instead of "No results yet" (#1860)
This commit is contained in:
commit
1e0fd88956
7 changed files with 135 additions and 6 deletions
15
README.md
15
README.md
|
|
@ -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) |
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 () => {
|
||||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import {
|
|||
Paper,
|
||||
ScrollArea,
|
||||
Stack,
|
||||
Text,
|
||||
Title,
|
||||
} from "@mantine/core";
|
||||
import type {
|
||||
|
|
@ -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,
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -18,6 +18,8 @@ import {
|
|||
createMrtrSamplingTool,
|
||||
createMrtrLoopTool,
|
||||
createMrtrEdgeCaseTool,
|
||||
loadConfig,
|
||||
resolveConfig,
|
||||
} from "@modelcontextprotocol/inspector-test-server";
|
||||
import type { ServerConfig } from "@modelcontextprotocol/inspector-test-server";
|
||||
import type {
|
||||
|
|
@ -26,6 +28,13 @@ import type {
|
|||
} from "@modelcontextprotocol/client";
|
||||
import { LOG_LEVEL_META_KEY } from "@modelcontextprotocol/client";
|
||||
import type { MessageEntry } from "@inspector/core/mcp/types.js";
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
// clients/web/src/test/integration/mcp → repo root is six levels up. Derived
|
||||
// from the module URL rather than `process.cwd()` so the showcase config
|
||||
// resolves the same however vitest is invoked.
|
||||
const repoRoot = join(dirname(fileURLToPath(import.meta.url)), "../".repeat(6));
|
||||
|
||||
/**
|
||||
* Live coverage of the modern (2026-07-28) connection path (#1700). The bundled
|
||||
|
|
@ -389,6 +398,45 @@ describe("modern-era negotiation (2026-07-28)", () => {
|
|||
expect(Object.keys(retry2.inputResponses ?? {})).toHaveLength(0);
|
||||
});
|
||||
|
||||
// #1860. Drives the reproduction the README documents, through the SAME
|
||||
// artifacts a human would use — the showcase config file, resolved through the
|
||||
// preset registry — so the config entry, the `mrtr_empty` registry branch, and
|
||||
// the handler are all covered rather than just the fixture factory. The
|
||||
// assertion is that a completed MRTR call really does hand back an empty
|
||||
// `CallToolResult`: that is the input the Tools panel has to render as
|
||||
// "completed, returned nothing" instead of its pre-run placeholder.
|
||||
it("completes an MRTR sequence with an empty result (mrtr_empty, via the showcase config)", async () => {
|
||||
const config = loadConfig(
|
||||
join(repoRoot, "test-servers/configs/mrtr-showcase-http.json"),
|
||||
);
|
||||
expect(config.tools).toContainEqual({ preset: "mrtr_empty" });
|
||||
|
||||
const started = createTestServerHttp(resolveConfig(config));
|
||||
await started.start();
|
||||
server = started;
|
||||
const connected = await connectWithEra(started.url, "modern");
|
||||
|
||||
let pausedAtPendingUi = false;
|
||||
connected.addEventListener("newPendingElicitation", (event) => {
|
||||
pausedAtPendingUi = true;
|
||||
void event.detail.respond({
|
||||
action: "accept",
|
||||
content: { ack: true },
|
||||
});
|
||||
});
|
||||
|
||||
const { tools } = await connected.listTools();
|
||||
const tool = tools.find((t) => t.name === "mrtr_empty");
|
||||
expect(tool).toBeDefined();
|
||||
|
||||
const result = await connected.callTool(tool!, {});
|
||||
expect(result.success).toBe(true);
|
||||
expect(pausedAtPendingUi).toBe(true);
|
||||
expect(result.result!.content).toEqual([]);
|
||||
expect(result.result!.structuredContent).toBeUndefined();
|
||||
expect(result.result!.isError).toBeFalsy();
|
||||
});
|
||||
|
||||
it("cancels an in-flight MRTR call while its embedded request is pending", async () => {
|
||||
const started = await startMrtrServer(createMrtrTool());
|
||||
const connected = await connectWithEra(started.url, "modern");
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
{ "preset": "mrtr_sample" },
|
||||
{ "preset": "mrtr_roots" },
|
||||
{ "preset": "mrtr_edge" },
|
||||
{ "preset": "mrtr_empty" },
|
||||
{ "preset": "mrtr_loop" }
|
||||
],
|
||||
"transport": {
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ import {
|
|||
createMrtrSamplingTool,
|
||||
createMrtrLoopTool,
|
||||
createMrtrEdgeCaseTool,
|
||||
createMrtrEmptyResultTool,
|
||||
createCollectUrlElicitationTool,
|
||||
createUrlElicitationFormTool,
|
||||
createSendNotificationTool,
|
||||
|
|
@ -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":
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
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
|
||||
|
|
|
|||
Loading…
Reference in a new issue