Skip to content

feat(web): gate header tabs on advertised server capabilities (#1516)#1520

Open
cliffhall wants to merge 2 commits into
v2/mainfrom
v2/1516-gate-tabs-on-capabilities
Open

feat(web): gate header tabs on advertised server capabilities (#1516)#1520
cliffhall wants to merge 2 commits into
v2/mainfrom
v2/1516-gate-tabs-on-capabilities

Conversation

@cliffhall

Copy link
Copy Markdown
Member

Summary

Fixes #1516. 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.

The reported bug: the Tools and Logs tabs were always shown when connected, even against a server that doesn't advertise 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, and the intent (gate on capability) wasn't expressed.

What changed

InspectorView.availableTabs now gates each server-capability tab on the matching field of initializeResult.capabilities. The capability object already rides on initializeResult (App builds it from the handshake and already passes it in), so no new prop/threading was needed — the connected header only renders when initializeResult is truthy, so it stays consistent.

Tab Gated on
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)
Servers / History always — client-side, never gated
Network transport-gated (hidden for stdio), unchanged

Client-side features (Sampling/Elicitation/Roots, when added) are inspector-offered client capabilities and must not be gated here. Tabs recompute live when reconnecting to a server with a different capability set.

Testing

  • Reworked the content-gating component tests into capability-gating tests: Tools-hidden regression (even with a non-empty tool list), Logs present/absent, capability-present-but-empty-list shows the tab, History-always-available under an empty capability set, Apps-hidden-without-tools-capability, and a reconnect-recomputes-tabs test.
  • Connected story/fixtures advertise a full capability set so the Connected story still renders every tab.
  • npm run validate (web): format, lint, build, 2003 unit tests pass; 370 Storybook play-function tests pass. InspectorView per-file coverage above gate (lines 96.7%, branches 98.2%, functions 81.8%).

Manual verification

Drove the web Inspector against three stdio test servers with distinct advertised capability sets:

  • tools onlyServers · Tools · History (Logs correctly absent)
  • prompts + logging, no toolsServers · Prompts · Logs · History (Tools correctly absent — the reported bug)
  • full capsServers · Tools · Prompts · Resources · Tasks · Logs · History (Tasks shown with zero tasks, proving capability-gating over content-gating)

🤖 Generated with Claude Code

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>
@cliffhall cliffhall added the v2 Issues and PRs for v2 label Jun 25, 2026
@cliffhall

Copy link
Copy Markdown
Member Author

Related to #1522 (fixes #1521). Both PRs touch clients/web/src/components/views/InspectorView/InspectorView.tsx and its test file, but in non-overlapping regions:

They should merge cleanly in either order; whichever lands second is worth a quick rebase check.

@cliffhall

Copy link
Copy Markdown
Member Author

@claude review

@claude

claude Bot commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Claude finished @cliffhall's task in 2m 58s —— View job


Review: gate header tabs on advertised capabilities (#1520)

  • Read changed files & diff
  • Analyze capability-gating logic
  • Review tests & stories
  • Post review feedback

Solid, well-scoped change. The core move — gating each header tab on initializeResult.capabilities.<x> instead of on list content — is correct and consistent with how core already interprets capabilities (inspectorClient.ts keys off this.capabilities?.tasks, test servers advertise capabilities.tasks/.tools/etc.). The !== undefined checks are the right test since advertised capabilities are objects ({}), and initializeResult is memoized in App.tsx:1136, so the new object memo dep won't cause extra recomputes. Comment block is verbose but matches this file's house style.

One substantive point worth discussing — Tasks gating granularity

InspectorView.tsx:535 gates the Tasks tab on the broad capabilities?.tasks !== undefined, but the Tasks screen renders requestor tasks (tasks is sourced from useManagedRequestorTasks, App.tsx:763), and requestor tasks only exist when the server advertises tasks.requests.tools.call — which is exactly what the run-as-task affordance keys off (App.tsx:1673, 2600), and what this PR's own comment cites at InspectorView.tsx:512-513.

A server that advertises tasks only for the receiver side (server-initiated createMessage/elicit polling — tasks.list/tasks.cancel, see inspectorClient.ts:1256-1261) would set capabilities.tasks but have no requests.tools.call. In that case this gate shows a Tasks tab that can never be non-empty — reintroducing, for that narrower server class, the very "empty unreachable screen" the PR is eliminating everywhere else.

Consider tightening to match the screen's actual data source and the affordance:

const hasTasks = capabilities?.tasks?.requests?.tools?.call !== undefined;

Not a blocker — it's a judgment call on whether you want the tab to mirror "server supports tasks at all" vs. "the inspector can create the tasks this screen shows." But given the affordance already uses the narrower path, the broader gate here is a slight inconsistency. Fix this →

Smaller notes

  • Removed "reveals tab live via list-changed" tests (Prompts/Resources/Tasks) — correct to drop, since gating no longer depends on content arriving. The new "reconnect recomputes tabs from the new capability set" test is a good replacement and covers the live-recompute path.
  • Apps keeping its content check (hasTools && appTools.length > 0) is the right call — Apps is a filtered view of tools, not its own capability — and the "app tools present but tools capability absent → hidden" test nails the edge.
  • No concern on the empty-{} capability degradation path; the initWithCapabilities({}) test confirms only Servers/History survive, which is the intended graceful fallback.

Nice test restructuring overall — the regression (Tools hidden despite a non-empty list) and the capability-present-but-empty-list cases are both explicitly covered.
· branch v2/1516-gate-tabs-on-capabilities

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

v2 Issues and PRs for v2

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant