Header navigation tabs now appear only when the connected server
advertises the corresponding server capability (from the MCP
`initialize` result `capabilities`), instead of being shown
unconditionally or proxied through list content.
Previously the Tools and Logs tabs were always shown when connected,
regardless of whether the server advertised `tools` / `logging`. The
content-gated tabs (Prompts/Resources/Tasks) only appeared correct
because an unsupported capability yields an empty list — but a server
advertising a capability with a currently-empty list would have its
tab wrongly hidden.
InspectorView.availableTabs now gates each server-capability tab on
the matching field of `initializeResult.capabilities` (already
threaded from App via the handshake):
Tools -> capabilities.tools
Logs -> capabilities.logging
Prompts -> capabilities.prompts
Resources -> capabilities.resources
Tasks -> capabilities.tasks
Apps -> capabilities.tools AND >=1 app tool (Apps is a filtered
view of tools, so it keeps its content check)
Client-side sections are never gated on server capabilities: Servers
and History are always available, and Network stays transport-gated
(hidden for stdio). Tabs recompute live when reconnecting to a server
with a different capability set.
Tests: reworked the content-gating component tests into
capability-gating tests (including the Tools-hidden regression, Logs
present/absent, capability-present-but-empty-list, History-always, and
a reconnect-recomputes test). Connected story/fixtures advertise a
full capability set so the Connected story still shows every tab.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
7330b326f0
commit
6d07f6ec75
3 changed files with 230 additions and 111 deletions
|
|
@ -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 = {
|
const demoInitializeResult: InitializeResult = {
|
||||||
protocolVersion: "2025-06-18",
|
protocolVersion: "2025-06-18",
|
||||||
capabilities: {},
|
capabilities: {
|
||||||
|
tools: {},
|
||||||
|
prompts: {},
|
||||||
|
resources: {},
|
||||||
|
logging: {},
|
||||||
|
tasks: {},
|
||||||
|
},
|
||||||
serverInfo: { name: "Local Dev Server", version: "1.2.0" },
|
serverInfo: { name: "Local Dev Server", version: "1.2.0" },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import type {
|
||||||
InitializeResult,
|
InitializeResult,
|
||||||
Prompt,
|
Prompt,
|
||||||
Resource,
|
Resource,
|
||||||
|
ServerCapabilities,
|
||||||
Task,
|
Task,
|
||||||
Tool,
|
Tool,
|
||||||
} from "@modelcontextprotocol/sdk/types.js";
|
} from "@modelcontextprotocol/sdk/types.js";
|
||||||
|
|
@ -154,12 +155,34 @@ const sampleServer: ServerEntry = {
|
||||||
connection: { status: "disconnected" },
|
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 = {
|
const connectedInit: InitializeResult = {
|
||||||
protocolVersion: "2025-06-18",
|
protocolVersion: "2025-06-18",
|
||||||
capabilities: {},
|
capabilities: allCapabilities,
|
||||||
serverInfo: { name: "Alpha", version: "1.0.0" },
|
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`),
|
// A tool the `isAppTool` filter recognizes (it carries `_meta.ui.resourceUri`),
|
||||||
// so its presence in the tool list makes the Apps tab available (#1450).
|
// so its presence in the tool list makes the Apps tab available (#1450).
|
||||||
const sampleAppTool: Tool = {
|
const sampleAppTool: Tool = {
|
||||||
|
|
@ -169,9 +192,9 @@ const sampleAppTool: Tool = {
|
||||||
_meta: { ui: { resourceUri: "ui://apps/ops" } },
|
_meta: { ui: { resourceUri: "ui://apps/ops" } },
|
||||||
};
|
};
|
||||||
|
|
||||||
// Prompts, Resources, and Tasks tabs are content-gated like Apps (#1450):
|
// Prompts, Resources, and Tasks tabs are gated on the server's advertised
|
||||||
// each is hidden until its list has an entry. These fixtures populate the
|
// capability (#1516), not on content. These fixtures populate the lists where
|
||||||
// lists so the associated tab is available.
|
// a test needs an entry rendered on the screen (e.g. list-changed indicator).
|
||||||
const samplePrompt: Prompt = { name: "greet" };
|
const samplePrompt: Prompt = { name: "greet" };
|
||||||
const sampleResource: Resource = {
|
const sampleResource: Resource = {
|
||||||
uri: "file:///readme.md",
|
uri: "file:///readme.md",
|
||||||
|
|
@ -380,6 +403,116 @@ describe("InspectorView", () => {
|
||||||
expect(labels).toContain("Network");
|
expect(labels).toContain("Network");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("hides the Tools tab when the server does not advertise the tools capability", async () => {
|
||||||
|
renderWithMantine(
|
||||||
|
<InspectorView
|
||||||
|
{...makeProps({
|
||||||
|
servers: [sampleServer],
|
||||||
|
activeServer: "alpha",
|
||||||
|
connectionStatus: "connected",
|
||||||
|
// No `tools` capability — only logging is advertised.
|
||||||
|
initializeResult: initWithCapabilities({ logging: {} }),
|
||||||
|
// A non-empty tool list must not override the missing capability.
|
||||||
|
tools: [{ name: "echo", inputSchema: { type: "object" } }],
|
||||||
|
})}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
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(
|
||||||
|
<InspectorView
|
||||||
|
{...makeProps({
|
||||||
|
servers: [sampleServer],
|
||||||
|
activeServer: "alpha",
|
||||||
|
connectionStatus: "connected",
|
||||||
|
initializeResult: initWithCapabilities({ tools: {} }),
|
||||||
|
tools: [],
|
||||||
|
})}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
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(
|
||||||
|
<InspectorView
|
||||||
|
{...makeProps({
|
||||||
|
servers: [sampleServer],
|
||||||
|
activeServer: "alpha",
|
||||||
|
connectionStatus: "connected",
|
||||||
|
initializeResult: initWithCapabilities({ tools: {} }),
|
||||||
|
})}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
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(
|
||||||
|
<InspectorView
|
||||||
|
{...makeProps({
|
||||||
|
servers: [sampleServer],
|
||||||
|
activeServer: "alpha",
|
||||||
|
connectionStatus: "connected",
|
||||||
|
initializeResult: initWithCapabilities({ logging: {} }),
|
||||||
|
})}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
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(
|
||||||
|
<InspectorView
|
||||||
|
{...makeProps({
|
||||||
|
servers: [sampleServer],
|
||||||
|
activeServer: "alpha",
|
||||||
|
connectionStatus: "connected",
|
||||||
|
// Empty capability set: every server-capability tab is hidden.
|
||||||
|
initializeResult: initWithCapabilities({}),
|
||||||
|
})}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
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(
|
||||||
|
<InspectorView
|
||||||
|
{...makeProps({
|
||||||
|
servers: [sampleServer],
|
||||||
|
activeServer: "alpha",
|
||||||
|
connectionStatus: "connected",
|
||||||
|
// Logging only — no tools capability, even though an app tool is
|
||||||
|
// present in the (stale/optimistic) list.
|
||||||
|
initializeResult: initWithCapabilities({ logging: {} }),
|
||||||
|
tools: [sampleAppTool],
|
||||||
|
})}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
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 () => {
|
it("filters tools to apps and auto-launches a no-fields app on the Apps tab", async () => {
|
||||||
const user = userEvent.setup();
|
const user = userEvent.setup();
|
||||||
// Plain (non-app) tool plus a tool with a malformed UI resource URI
|
// Plain (non-app) tool plus a tool with a malformed UI resource URI
|
||||||
|
|
@ -535,15 +668,18 @@ describe("InspectorView", () => {
|
||||||
expect(screen.getByDisplayValue("Servers")).toBeInTheDocument();
|
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(
|
renderWithMantine(
|
||||||
<InspectorView
|
<InspectorView
|
||||||
{...makeProps({
|
{...makeProps({
|
||||||
servers: [sampleServer],
|
servers: [sampleServer],
|
||||||
activeServer: "alpha",
|
activeServer: "alpha",
|
||||||
connectionStatus: "connected",
|
connectionStatus: "connected",
|
||||||
initializeResult: connectedInit,
|
// Advertise tools but not prompts.
|
||||||
prompts: [],
|
initializeResult: initWithCapabilities({ tools: {} }),
|
||||||
|
// Content is irrelevant to gating now — even a populated list stays
|
||||||
|
// hidden when the capability is absent.
|
||||||
|
prompts: [samplePrompt],
|
||||||
})}
|
})}
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
|
@ -553,48 +689,35 @@ describe("InspectorView", () => {
|
||||||
expect(labels).not.toContain("Prompts");
|
expect(labels).not.toContain("Prompts");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("reveals the Prompts tab live when a prompt arrives via list-changed refresh", async () => {
|
it("shows the Prompts tab when the server advertises prompts even with an empty list", async () => {
|
||||||
const { rerender } = renderWithMantine(
|
|
||||||
<InspectorView
|
|
||||||
{...makeProps({
|
|
||||||
servers: [sampleServer],
|
|
||||||
activeServer: "alpha",
|
|
||||||
connectionStatus: "connected",
|
|
||||||
initializeResult: connectedInit,
|
|
||||||
prompts: [],
|
|
||||||
})}
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
let radios = await screen.findAllByRole("radio");
|
|
||||||
expect(radios.map((r) => r.getAttribute("value"))).not.toContain("Prompts");
|
|
||||||
|
|
||||||
rerender(
|
|
||||||
<InspectorView
|
|
||||||
{...makeProps({
|
|
||||||
servers: [sampleServer],
|
|
||||||
activeServer: "alpha",
|
|
||||||
connectionStatus: "connected",
|
|
||||||
initializeResult: connectedInit,
|
|
||||||
prompts: [samplePrompt],
|
|
||||||
})}
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
await waitFor(async () => {
|
|
||||||
radios = await screen.findAllByRole("radio");
|
|
||||||
expect(radios.map((r) => r.getAttribute("value"))).toContain("Prompts");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("hides the Resources tab when the server exposes no resources or templates", async () => {
|
|
||||||
renderWithMantine(
|
renderWithMantine(
|
||||||
<InspectorView
|
<InspectorView
|
||||||
{...makeProps({
|
{...makeProps({
|
||||||
servers: [sampleServer],
|
servers: [sampleServer],
|
||||||
activeServer: "alpha",
|
activeServer: "alpha",
|
||||||
connectionStatus: "connected",
|
connectionStatus: "connected",
|
||||||
initializeResult: connectedInit,
|
initializeResult: initWithCapabilities({ prompts: {} }),
|
||||||
resources: [],
|
// No prompts yet — the tab is still available because the server
|
||||||
resourceTemplates: [],
|
// advertises the capability (#1516).
|
||||||
|
prompts: [],
|
||||||
|
})}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
const radios = await screen.findAllByRole("radio");
|
||||||
|
expect(radios.map((r) => r.getAttribute("value"))).toContain("Prompts");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("hides the Resources tab when the server does not advertise the resources capability", async () => {
|
||||||
|
renderWithMantine(
|
||||||
|
<InspectorView
|
||||||
|
{...makeProps({
|
||||||
|
servers: [sampleServer],
|
||||||
|
activeServer: "alpha",
|
||||||
|
connectionStatus: "connected",
|
||||||
|
initializeResult: initWithCapabilities({ tools: {} }),
|
||||||
|
// Populated lists are ignored when the capability is absent.
|
||||||
|
resources: [sampleResource],
|
||||||
|
resourceTemplates: [{ uriTemplate: "file:///{path}", name: "Files" }],
|
||||||
})}
|
})}
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
|
@ -604,16 +727,16 @@ describe("InspectorView", () => {
|
||||||
expect(labels).not.toContain("Resources");
|
expect(labels).not.toContain("Resources");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("shows the Resources tab when the server exposes only resource templates", async () => {
|
it("shows the Resources tab when the server advertises resources even with empty lists", async () => {
|
||||||
renderWithMantine(
|
renderWithMantine(
|
||||||
<InspectorView
|
<InspectorView
|
||||||
{...makeProps({
|
{...makeProps({
|
||||||
servers: [sampleServer],
|
servers: [sampleServer],
|
||||||
activeServer: "alpha",
|
activeServer: "alpha",
|
||||||
connectionStatus: "connected",
|
connectionStatus: "connected",
|
||||||
initializeResult: connectedInit,
|
initializeResult: initWithCapabilities({ resources: {} }),
|
||||||
resources: [],
|
resources: [],
|
||||||
resourceTemplates: [{ uriTemplate: "file:///{path}", name: "Files" }],
|
resourceTemplates: [],
|
||||||
})}
|
})}
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
|
@ -621,51 +744,16 @@ describe("InspectorView", () => {
|
||||||
expect(radios.map((r) => r.getAttribute("value"))).toContain("Resources");
|
expect(radios.map((r) => r.getAttribute("value"))).toContain("Resources");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("reveals the Resources tab live when a resource arrives via list-changed refresh", async () => {
|
it("hides the Tasks tab when the server does not advertise the tasks capability", async () => {
|
||||||
const { rerender } = renderWithMantine(
|
|
||||||
<InspectorView
|
|
||||||
{...makeProps({
|
|
||||||
servers: [sampleServer],
|
|
||||||
activeServer: "alpha",
|
|
||||||
connectionStatus: "connected",
|
|
||||||
initializeResult: connectedInit,
|
|
||||||
resources: [],
|
|
||||||
resourceTemplates: [],
|
|
||||||
})}
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
let radios = await screen.findAllByRole("radio");
|
|
||||||
expect(radios.map((r) => r.getAttribute("value"))).not.toContain(
|
|
||||||
"Resources",
|
|
||||||
);
|
|
||||||
|
|
||||||
rerender(
|
|
||||||
<InspectorView
|
|
||||||
{...makeProps({
|
|
||||||
servers: [sampleServer],
|
|
||||||
activeServer: "alpha",
|
|
||||||
connectionStatus: "connected",
|
|
||||||
initializeResult: connectedInit,
|
|
||||||
resources: [sampleResource],
|
|
||||||
resourceTemplates: [],
|
|
||||||
})}
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
await waitFor(async () => {
|
|
||||||
radios = await screen.findAllByRole("radio");
|
|
||||||
expect(radios.map((r) => r.getAttribute("value"))).toContain("Resources");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("hides the Tasks tab when the server has created no tasks", async () => {
|
|
||||||
renderWithMantine(
|
renderWithMantine(
|
||||||
<InspectorView
|
<InspectorView
|
||||||
{...makeProps({
|
{...makeProps({
|
||||||
servers: [sampleServer],
|
servers: [sampleServer],
|
||||||
activeServer: "alpha",
|
activeServer: "alpha",
|
||||||
connectionStatus: "connected",
|
connectionStatus: "connected",
|
||||||
initializeResult: connectedInit,
|
initializeResult: initWithCapabilities({ tools: {} }),
|
||||||
tasks: [],
|
// An existing task is ignored when the capability is absent.
|
||||||
|
tasks: [sampleTask],
|
||||||
})}
|
})}
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
|
@ -675,15 +763,15 @@ describe("InspectorView", () => {
|
||||||
expect(labels).not.toContain("Tasks");
|
expect(labels).not.toContain("Tasks");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("shows the Tasks tab when at least one task exists", async () => {
|
it("shows the Tasks tab when the server advertises tasks even with no tasks yet", async () => {
|
||||||
renderWithMantine(
|
renderWithMantine(
|
||||||
<InspectorView
|
<InspectorView
|
||||||
{...makeProps({
|
{...makeProps({
|
||||||
servers: [sampleServer],
|
servers: [sampleServer],
|
||||||
activeServer: "alpha",
|
activeServer: "alpha",
|
||||||
connectionStatus: "connected",
|
connectionStatus: "connected",
|
||||||
initializeResult: connectedInit,
|
initializeResult: initWithCapabilities({ tasks: {} }),
|
||||||
tasks: [sampleTask],
|
tasks: [],
|
||||||
})}
|
})}
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
|
@ -691,35 +779,40 @@ describe("InspectorView", () => {
|
||||||
expect(radios.map((r) => r.getAttribute("value"))).toContain("Tasks");
|
expect(radios.map((r) => r.getAttribute("value"))).toContain("Tasks");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("reveals the Tasks tab live when a task is created", async () => {
|
it("recomputes tabs from the new capability set when reconnecting to a different server", async () => {
|
||||||
|
// First server advertises tasks but not logging.
|
||||||
const { rerender } = renderWithMantine(
|
const { rerender } = renderWithMantine(
|
||||||
<InspectorView
|
<InspectorView
|
||||||
{...makeProps({
|
{...makeProps({
|
||||||
servers: [sampleServer],
|
servers: [sampleServer],
|
||||||
activeServer: "alpha",
|
activeServer: "alpha",
|
||||||
connectionStatus: "connected",
|
connectionStatus: "connected",
|
||||||
initializeResult: connectedInit,
|
initializeResult: initWithCapabilities({ tools: {}, tasks: {} }),
|
||||||
tasks: [],
|
|
||||||
})}
|
})}
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
let radios = await screen.findAllByRole("radio");
|
let radios = await screen.findAllByRole("radio");
|
||||||
expect(radios.map((r) => r.getAttribute("value"))).not.toContain("Tasks");
|
let labels = radios.map((r) => r.getAttribute("value"));
|
||||||
|
expect(labels).toContain("Tasks");
|
||||||
|
expect(labels).not.toContain("Logs");
|
||||||
|
|
||||||
|
// Reconnect to a server that advertises logging but not tasks — the tabs
|
||||||
|
// recompute purely from the new capability set.
|
||||||
rerender(
|
rerender(
|
||||||
<InspectorView
|
<InspectorView
|
||||||
{...makeProps({
|
{...makeProps({
|
||||||
servers: [sampleServer],
|
servers: [sampleServer],
|
||||||
activeServer: "alpha",
|
activeServer: "alpha",
|
||||||
connectionStatus: "connected",
|
connectionStatus: "connected",
|
||||||
initializeResult: connectedInit,
|
initializeResult: initWithCapabilities({ tools: {}, logging: {} }),
|
||||||
tasks: [sampleTask],
|
|
||||||
})}
|
})}
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
await waitFor(async () => {
|
await waitFor(async () => {
|
||||||
radios = await screen.findAllByRole("radio");
|
radios = await screen.findAllByRole("radio");
|
||||||
expect(radios.map((r) => r.getAttribute("value"))).toContain("Tasks");
|
labels = radios.map((r) => r.getAttribute("value"));
|
||||||
|
expect(labels).toContain("Logs");
|
||||||
|
expect(labels).not.toContain("Tasks");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -1025,7 +1118,8 @@ describe("InspectorView", () => {
|
||||||
activeServer: "alpha",
|
activeServer: "alpha",
|
||||||
connectionStatus: "connected",
|
connectionStatus: "connected",
|
||||||
initializeResult: connectedInit,
|
initializeResult: connectedInit,
|
||||||
// An app tool is required for the Apps tab to be available (#1450).
|
// An app tool is required for the Apps tab to be available — Apps
|
||||||
|
// keeps a content check on top of the tools capability (#1516).
|
||||||
tools: [sampleAppTool],
|
tools: [sampleAppTool],
|
||||||
toolsListChanged: true,
|
toolsListChanged: true,
|
||||||
})}
|
})}
|
||||||
|
|
@ -1043,7 +1137,8 @@ describe("InspectorView", () => {
|
||||||
activeServer: "alpha",
|
activeServer: "alpha",
|
||||||
connectionStatus: "connected",
|
connectionStatus: "connected",
|
||||||
initializeResult: connectedInit,
|
initializeResult: connectedInit,
|
||||||
// A prompt is required for the Prompts tab to be available (#1450).
|
// connectedInit advertises prompts, so the tab is available; the
|
||||||
|
// prompt populates the screen so the indicator has a list to mark.
|
||||||
prompts: [samplePrompt],
|
prompts: [samplePrompt],
|
||||||
promptsListChanged: true,
|
promptsListChanged: true,
|
||||||
})}
|
})}
|
||||||
|
|
@ -1061,7 +1156,8 @@ describe("InspectorView", () => {
|
||||||
activeServer: "alpha",
|
activeServer: "alpha",
|
||||||
connectionStatus: "connected",
|
connectionStatus: "connected",
|
||||||
initializeResult: connectedInit,
|
initializeResult: connectedInit,
|
||||||
// A resource is required for the Resources tab to be available (#1450).
|
// connectedInit advertises resources, so the tab is available; the
|
||||||
|
// resource populates the screen so the indicator has a list to mark.
|
||||||
resources: [sampleResource],
|
resources: [sampleResource],
|
||||||
resourcesListChanged: true,
|
resourcesListChanged: true,
|
||||||
})}
|
})}
|
||||||
|
|
@ -1079,7 +1175,7 @@ describe("InspectorView", () => {
|
||||||
activeServer: "alpha",
|
activeServer: "alpha",
|
||||||
connectionStatus: "connected",
|
connectionStatus: "connected",
|
||||||
initializeResult: connectedInit,
|
initializeResult: connectedInit,
|
||||||
// A prompt is required for the Prompts tab to be available (#1450).
|
// connectedInit advertises prompts, so the Prompts tab is available.
|
||||||
prompts: [samplePrompt],
|
prompts: [samplePrompt],
|
||||||
// Tools changed, but Prompts did not — the Prompts screen must
|
// Tools changed, but Prompts did not — the Prompts screen must
|
||||||
// stay quiet.
|
// stay quiet.
|
||||||
|
|
|
||||||
|
|
@ -496,45 +496,60 @@ export function InspectorView({
|
||||||
});
|
});
|
||||||
}, [tools]);
|
}, [tools]);
|
||||||
|
|
||||||
// Only show the non-Servers tabs when actually connected. Network is
|
// Only show the non-Servers tabs when actually connected. Each
|
||||||
// additionally hidden for stdio servers — there is no HTTP traffic to
|
// server-capability tab is gated on the matching field of the server's
|
||||||
// surface there, so the tab would always be empty. Apps, Prompts,
|
// advertised `capabilities` (from the MCP `initialize` result), not on
|
||||||
// Resources, and Tasks are content-gated (#1450): each is hidden unless its
|
// current content (#1516): a server that advertises a capability but
|
||||||
// list has at least one entry, so an empty screen is never reachable.
|
// currently has zero items still shows its tab, and a server that doesn't
|
||||||
// Resources is gated on resources OR templates, since a server may expose
|
// advertise the capability never does — even if a stale/optimistic list is
|
||||||
// only templates; Tasks appear once a task-augmented tool call creates one
|
// briefly non-empty. The capability object rides along on `initializeResult`
|
||||||
// (the "run as task" affordance lives on the Tools screen, gated by the
|
// (App builds it from the handshake), which is only truthy while connected.
|
||||||
// server's task support). These memo dependencies make the tabs
|
//
|
||||||
// appear/disappear live as the lists change (list-changed refresh, server
|
// Tools → capabilities.tools
|
||||||
// switch) — when app tools exist but the sandbox is unavailable the Apps
|
// Logs → capabilities.logging
|
||||||
// tab stays visible so its "unavailable" message remains reachable. Users
|
// Prompts → capabilities.prompts
|
||||||
// who want to inspect a server's advertised capabilities regardless of
|
// Resources → capabilities.resources
|
||||||
// current contents can open the Connection Info modal.
|
// Tasks → capabilities.tasks (the "run as task" affordance on the
|
||||||
|
// Tools screen separately keys off tasks.requests.tools.call)
|
||||||
|
// Apps → capabilities.tools (MCP Apps build on tools) AND at least one
|
||||||
|
// app tool — Apps is a filtered view of tools, not its own
|
||||||
|
// capability, so it keeps the content check; when app tools
|
||||||
|
// exist but the sandbox is unavailable the tab stays visible so
|
||||||
|
// its "unavailable" message remains reachable.
|
||||||
|
//
|
||||||
|
// Network is hidden for stdio servers (no HTTP traffic to surface).
|
||||||
|
// Servers and History are never capability-gated — History is a local
|
||||||
|
// client-side log, and any future client capabilities (sampling /
|
||||||
|
// elicitation / roots) are inspector-offered, not server-advertised, so
|
||||||
|
// they must not be gated here either. These memo dependencies make the tabs
|
||||||
|
// recompute live as capabilities change (server switch, reconnect).
|
||||||
const availableTabs = useMemo<string[]>(() => {
|
const availableTabs = useMemo<string[]>(() => {
|
||||||
if (connectionStatus !== "connected") return [SERVERS_TAB];
|
if (connectionStatus !== "connected") return [SERVERS_TAB];
|
||||||
const active = serversInput.find((s) => s.id === activeServer);
|
const active = serversInput.find((s) => s.id === activeServer);
|
||||||
const isStdio = active ? getServerType(active.config) === "stdio" : false;
|
const isStdio = active ? getServerType(active.config) === "stdio" : false;
|
||||||
const hasApps = appTools.length > 0;
|
const capabilities = initializeResult?.capabilities;
|
||||||
const hasPrompts = prompts.length > 0;
|
const hasTools = capabilities?.tools !== undefined;
|
||||||
const hasResources = resources.length > 0 || resourceTemplates.length > 0;
|
const hasApps = hasTools && appTools.length > 0;
|
||||||
const hasTasks = tasks.length > 0;
|
const hasPrompts = capabilities?.prompts !== undefined;
|
||||||
|
const hasResources = capabilities?.resources !== undefined;
|
||||||
|
const hasTasks = capabilities?.tasks !== undefined;
|
||||||
|
const hasLogging = capabilities?.logging !== undefined;
|
||||||
return ALL_TABS.filter((t) => {
|
return ALL_TABS.filter((t) => {
|
||||||
if (t === NETWORK_TAB && isStdio) return false;
|
if (t === NETWORK_TAB && isStdio) return false;
|
||||||
|
if (t === "Tools" && !hasTools) return false;
|
||||||
if (t === "Apps" && !hasApps) return false;
|
if (t === "Apps" && !hasApps) return false;
|
||||||
if (t === "Prompts" && !hasPrompts) return false;
|
if (t === "Prompts" && !hasPrompts) return false;
|
||||||
if (t === "Resources" && !hasResources) return false;
|
if (t === "Resources" && !hasResources) return false;
|
||||||
if (t === "Tasks" && !hasTasks) return false;
|
if (t === "Tasks" && !hasTasks) return false;
|
||||||
|
if (t === "Logs" && !hasLogging) return false;
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
}, [
|
}, [
|
||||||
connectionStatus,
|
connectionStatus,
|
||||||
serversInput,
|
serversInput,
|
||||||
activeServer,
|
activeServer,
|
||||||
|
initializeResult,
|
||||||
appTools,
|
appTools,
|
||||||
prompts,
|
|
||||||
resources,
|
|
||||||
resourceTemplates,
|
|
||||||
tasks,
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Clamp the rendered tab to whatever's currently available. If the user
|
// Clamp the rendered tab to whatever's currently available. If the user
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue