feat(test-servers): mcp_app_demo preset + _meta on tool/resource defs (closes #1557)

Add an `mcp_app_demo` tool/resource preset — a self-contained MCP App widget
that exercises size-changed, ui/message, log notifications, and host-context
rendering with no external server. Requires plumbing an optional `_meta` field
through the composable test server's `ToolDefinition` and `ResourceDefinition`
so clients can read tool-level `_meta.ui.resourceUri` and resource-level
`_meta.ui.csp`.

- composable-test-server: optional `_meta` on ToolDefinition/ResourceDefinition,
  passed through to registerTool config and the resource content item.
- test-server-fixtures: `createMcpAppDemoTool()`, `createMcpAppDemoResource()`,
  inline `MCP_APP_DEMO_HTML` widget; wired into getDefaultServerConfig.
- preset-registry: `mcp_app_demo` tool + `mcp_app_demo_widget` resource presets.
- integration test asserting the `_meta` plumbing end-to-end.

Wave 1 of PR #1510 decomposition.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S3fTN8H3R8YV4yUGvZjYnX
This commit is contained in:
cliffhall 2026-07-01 18:22:25 -04:00
commit 0d50b908a3

View file

@ -0,0 +1,108 @@
import { describe, it, expect, afterEach } from "vitest";
import { InspectorClient } from "@inspector/core/mcp/inspectorClient.js";
import { createTransportNode } from "@inspector/core/mcp/node/transport.js";
import {
createTestServerHttp,
type TestServerHttp,
createTestServerInfo,
createMcpAppDemoTool,
createMcpAppDemoResource,
} from "@modelcontextprotocol/inspector-test-server";
import type { Tool } from "@modelcontextprotocol/sdk/types.js";
const MCP_APP_DEMO_URI = "ui://demo/widget.html";
/**
* Exercises the `mcp_app_demo` preset added in #1557 end-to-end: it confirms the
* `_meta` plumbing through the composable test server's tool/resource
* definitions surfaces on `tools/list` and `resources/read`, which is what the
* downstream Apps-host and CLI `--app-info` tests rely on.
*/
describe("mcp_app_demo preset", () => {
let client: InspectorClient | null = null;
let server: TestServerHttp | null = null;
afterEach(async () => {
if (client) {
try {
await client.disconnect();
} catch {
// Ignore disconnect errors
}
client = null;
}
if (server) {
try {
await server.stop();
} catch {
// Ignore server stop errors
}
server = null;
}
});
async function connect(): Promise<InspectorClient> {
server = createTestServerHttp({
serverInfo: createTestServerInfo("mcp-app-demo-test", "1.0.0"),
tools: [createMcpAppDemoTool()],
resources: [createMcpAppDemoResource()],
});
await server.start();
const connected = new InspectorClient(
{ type: "streamable-http", url: server.url },
{ environment: { transport: createTransportNode } },
);
await connected.connect();
client = connected;
return connected;
}
it("exposes tool-level _meta.ui.resourceUri on tools/list", async () => {
const connected = await connect();
const { tools } = await connected.listTools();
const tool = tools.find((t) => t.name === "mcp_app_demo");
expect(tool).toBeDefined();
const meta = (tool as Tool)._meta as
| { ui?: { resourceUri?: string; visibility?: string[] } }
| undefined;
expect(meta?.ui?.resourceUri).toBe(MCP_APP_DEMO_URI);
expect(meta?.ui?.visibility).toEqual(["model", "app"]);
});
it("exposes resource-level _meta.ui on resources/read", async () => {
const connected = await connect();
const invocation = await connected.readResource(MCP_APP_DEMO_URI);
const contents = invocation.result.contents;
expect(contents).toHaveLength(1);
const entry = contents[0];
expect(entry.mimeType).toBe("text/html");
expect("text" in entry ? String(entry.text) : "").toContain("mcp-app-demo");
const meta = entry._meta as
| {
ui?: {
csp?: { connectDomains?: string[]; resourceDomains?: string[] };
permissions?: { clipboard?: boolean };
prefersBorder?: boolean;
};
}
| undefined;
expect(meta?.ui?.csp).toEqual({
connectDomains: [],
resourceDomains: [],
});
expect(meta?.ui?.permissions).toEqual({ clipboard: false });
expect(meta?.ui?.prefersBorder).toBe(true);
});
it("echoes the input title from the tool handler", async () => {
const connected = await connect();
const { tools } = await connected.listTools();
const tool = tools.find((t) => t.name === "mcp_app_demo")!;
const result = await connected.callTool(tool, { title: "Widget A" });
expect(result.success).toBe(true);
const content = result.result!.content;
const text =
content[0] && "text" in content[0] ? String(content[0].text) : "";
expect(text).toBe('mcp_app_demo rendered with title="Widget A"');
});
});