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:
Cliff Hall 2026-08-16 23:20:29 -04:00 • committed by GitHub
commit 1e0fd88956
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 135 additions and 6 deletions

View file

@ -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();

View file

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

View file

@ -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");