diff --git a/clients/web/src/components/views/InspectorView/InspectorView.stories.tsx b/clients/web/src/components/views/InspectorView/InspectorView.stories.tsx index 108fdc987..13e4fb81c 100644 --- a/clients/web/src/components/views/InspectorView/InspectorView.stories.tsx +++ b/clients/web/src/components/views/InspectorView/InspectorView.stories.tsx @@ -294,9 +294,17 @@ const demoNetwork: FetchRequestEntry[] = [ }, ]; +// Advertise every primitive capability so the Connected story shows the full +// tab list — each header tab is gated on the matching capability field (#1516). const demoInitializeResult: InitializeResult = { protocolVersion: "2025-06-18", - capabilities: {}, + capabilities: { + tools: {}, + prompts: {}, + resources: {}, + logging: {}, + tasks: {}, + }, serverInfo: { name: "Local Dev Server", version: "1.2.0" }, }; diff --git a/clients/web/src/components/views/InspectorView/InspectorView.test.tsx b/clients/web/src/components/views/InspectorView/InspectorView.test.tsx index 18e1be168..102e3da9e 100644 --- a/clients/web/src/components/views/InspectorView/InspectorView.test.tsx +++ b/clients/web/src/components/views/InspectorView/InspectorView.test.tsx @@ -5,6 +5,7 @@ import type { InitializeResult, Prompt, Resource, + ServerCapabilities, Task, Tool, } from "@modelcontextprotocol/sdk/types.js"; @@ -154,12 +155,34 @@ const sampleServer: ServerEntry = { connection: { status: "disconnected" }, }; +// A server that advertises every primitive capability. Each header tab is +// gated on the matching capability field (#1516), so most connected-mode +// tests use this fixture to make the corresponding tabs present; the +// capability-gating tests below override `capabilities` to drop or restore +// individual fields. +const allCapabilities: ServerCapabilities = { + tools: {}, + prompts: {}, + resources: {}, + logging: {}, + tasks: {}, +}; + const connectedInit: InitializeResult = { protocolVersion: "2025-06-18", - capabilities: {}, + capabilities: allCapabilities, serverInfo: { name: "Alpha", version: "1.0.0" }, }; +// Builds an initialize result with a specific capability set, otherwise +// identical to `connectedInit`. Used by the capability-gating tests to assert +// a tab appears/disappears purely on the advertised capability. +function initWithCapabilities( + capabilities: ServerCapabilities, +): InitializeResult { + return { ...connectedInit, capabilities }; +} + // A tool the `isAppTool` filter recognizes (it carries `_meta.ui.resourceUri`), // so its presence in the tool list makes the Apps tab available (#1450). const sampleAppTool: Tool = { @@ -169,9 +192,9 @@ const sampleAppTool: Tool = { _meta: { ui: { resourceUri: "ui://apps/ops" } }, }; -// Prompts, Resources, and Tasks tabs are content-gated like Apps (#1450): -// each is hidden until its list has an entry. These fixtures populate the -// lists so the associated tab is available. +// Prompts, Resources, and Tasks tabs are gated on the server's advertised +// capability (#1516), not on content. These fixtures populate the lists where +// a test needs an entry rendered on the screen (e.g. list-changed indicator). const samplePrompt: Prompt = { name: "greet" }; const sampleResource: Resource = { uri: "file:///readme.md", @@ -380,6 +403,116 @@ describe("InspectorView", () => { expect(labels).toContain("Network"); }); + it("hides the Tools tab when the server does not advertise the tools capability", async () => { + renderWithMantine( + , + ); + const radios = await screen.findAllByRole("radio"); + const labels = radios.map((r) => r.getAttribute("value")); + expect(labels).not.toContain("Tools"); + // Sibling capability is independent — Logs is present, Apps stays hidden + // (Apps build on the tools capability). + expect(labels).toContain("Logs"); + expect(labels).not.toContain("Apps"); + }); + + it("shows the Tools tab when the server advertises tools even with an empty list", async () => { + renderWithMantine( + , + ); + const radios = await screen.findAllByRole("radio"); + expect(radios.map((r) => r.getAttribute("value"))).toContain("Tools"); + }); + + it("hides the Logs tab when the server does not advertise the logging capability", async () => { + renderWithMantine( + , + ); + const radios = await screen.findAllByRole("radio"); + const labels = radios.map((r) => r.getAttribute("value")); + expect(labels).toContain("Tools"); + expect(labels).not.toContain("Logs"); + }); + + it("shows the Logs tab when the server advertises the logging capability", async () => { + renderWithMantine( + , + ); + const radios = await screen.findAllByRole("radio"); + expect(radios.map((r) => r.getAttribute("value"))).toContain("Logs"); + }); + + it("keeps History available regardless of advertised server capabilities", async () => { + // History is a local client-side log — never gated on server capabilities. + renderWithMantine( + , + ); + const radios = await screen.findAllByRole("radio"); + const labels = radios.map((r) => r.getAttribute("value")); + expect(labels).toContain("Servers"); + expect(labels).toContain("History"); + expect(labels).not.toContain("Tools"); + expect(labels).not.toContain("Logs"); + }); + + it("hides the Apps tab when app tools exist but the server omits the tools capability", async () => { + renderWithMantine( + , + ); + const radios = await screen.findAllByRole("radio"); + expect(radios.map((r) => r.getAttribute("value"))).not.toContain("Apps"); + }); + it("filters tools to apps and auto-launches a no-fields app on the Apps tab", async () => { const user = userEvent.setup(); // Plain (non-app) tool plus a tool with a malformed UI resource URI @@ -535,15 +668,18 @@ describe("InspectorView", () => { expect(screen.getByDisplayValue("Servers")).toBeInTheDocument(); }); - it("hides the Prompts tab when the server exposes no prompts", async () => { + it("hides the Prompts tab when the server does not advertise the prompts capability", async () => { renderWithMantine(