Skip to content

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

Merged
cliffhall merged 2 commits into
v2/mainfrom
v2/1516-gate-tabs-on-capabilities
Jun 26, 2026
Merged

feat(web): gate header tabs on advertised server capabilities (#1516)#1520
cliffhall merged 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

@cliffhall

Copy link
Copy Markdown
Member Author

Thanks for the thorough review — and good catch on the Tasks granularity nuance.

On the Tasks gate: keeping the broad capabilities?.tasks !== undefined (intentional).

This is a deliberate spec decision rather than an oversight. Issue #1516 explicitly calls it out:

Tasks → capabilities?.tasks (and Run as Task already keys off tasks.requests.tools.call)

i.e. the tab gate (broad "server supports tasks") and the affordance gate (narrow "inspector can create task-augmented tool calls") are deliberately different concerns.

It's also consistent with the core's own list-RPC gating: ManagedRequestorTasksState short-circuits tasks/list on the broad client.getCapabilities()?.tasks (core/mcp/state/managedRequestorTasksState.ts:134), not on requests.tools.call. So a server advertising tasks can answer tasks/list, and the tab gate mirrors exactly the condition under which the screen issues its list RPC — keeping tab visibility and the screen's data fetch in lockstep. Tightening the tab to requests.tools.call while the list RPC still fires on broad tasks would actually desync those two.

You're right that a receiver-only-tasks server (tasks.list/tasks.cancel, no requests.tools.call) would land on a Tasks tab the inspector can't populate via run-as-task — but that's the narrower "capability advertised, list currently empty" case the PR intentionally accepts everywhere (the tab reflects the server's task surface; an empty list is a valid state, not an unreachable screen). The "Run as Task" affordance staying on the narrow key is what correctly prevents offering an action the server can't honor.

Leaving the gate as-is per the issue spec. Everything else in the review (Apps content-check, dropped list-changed tests, reconnect-recompute coverage, empty-{} fallback) — agreed, thanks for confirming.

@cliffhall

Copy link
Copy Markdown
Member Author

@claude review

@claude

claude Bot commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Claude finished @cliffhall's task in 1m 38s —— View job


Re-review: gate header tabs on advertised capabilities (#1520)

  • Read changed files & current diff
  • Re-analyze capability-gating logic (incl. Tasks decision)
  • Review tests & stories
  • Post review feedback

LGTM. Clean, well-scoped, and the test restructuring is thorough. Re-reviewed the current branch state including the follow-up to my earlier Tasks note.

Tasks granularity — resolved, and I verified the rationale

Your decision to keep the broad capabilities?.tasks !== undefined gate (rather than tightening to tasks.requests.tools.call) checks out. I confirmed the consistency argument against core: ManagedRequestorTasksState short-circuits tasks/list on exactly the broad capability —

// core/mcp/state/managedRequestorTasksState.ts:134
if (!client.getCapabilities()?.tasks) { this.tasks = [];  return; }

So the Tasks tab is visible under precisely the condition where the screen will actually issue its list RPC. Tightening the tab to requests.tools.call while the list call still fires on broad tasks would have desynced visibility from the data fetch. Keeping them in lockstep is the right call, and the inline comment now documents the split between tab gate (broad) and the run-as-task affordance (narrow tasks.requests.tools.call) — good.

Fresh pass — confirmations

  • !== undefined is the correct predicate. Advertised capabilities are objects ({}), so presence is the signal; capabilities?.tools !== undefined correctly yields false when capabilities itself is undefined (disconnected/no-handshake). InspectorView.tsx:530-536.
  • Logs → capabilities.logging is right. I checked the Logs screen (InspectorView.tsx:743): LoggingScreen renders server-emitted log entries plus the logging/setLevel control — it's wholly about server logging, so there are no client-side logs being hidden by this gate. (Client-side activity lives under History, which stays ungated.)
  • Resources → capabilities.resources. This subsumes the old "resources OR templates" content check correctly, since resource templates fall under the same resources capability per spec — the "advertises resources, empty lists" test confirms a template-only server still shows the tab.
  • Apps keeping hasTools && appTools.length > 0 is the correct exception — Apps is a filtered view of tools, not its own capability — and the "app tool present but tools capability absent → Apps hidden" test nails the edge.
  • Servers / History always ungated; Network transport-gated — unchanged and correct. The comment's note that future client capabilities (sampling/elicitation/roots) are inspector-offered and must not be gated here is a useful guardrail for the next contributor.

Tests & stories

  • The content→capability test rework is comprehensive: Tools-hidden-despite-non-empty-list (the reported regression), capability-present-but-empty-list-shows-tab for each primitive, empty-{} → only Servers/History survive, and the reconnect-recomputes-from-new-capability-set test cleanly replaces the dropped per-primitive "reveals live via list-changed" tests.
  • Dropping those list-changed reveal tests is correct now that visibility no longer depends on content arriving; the list-changed indicator tests were appropriately retargeted with comments noting connectedInit advertises the relevant capability.
  • Story fixture advertising the full capability set so Connected still renders every tab is the right minimal change.

No blocking issues, no nits worth holding the PR for. Nice work.
· branch v2/1516-gate-tabs-on-capabilities

@cliffhall

cliffhall commented Jun 26, 2026

Copy link
Copy Markdown
Member Author

Manual smoke test — capability gating verified in the running web Inspector

Drove the dev web Inspector (this branch) against two stdio composable test servers built from test-servers/ that differ only in their advertised initialize capabilities, to confirm the Tools/Logs tabs gate on capability rather than content.

Setup

Two configs for server-composable.js:

  • no-tools-no-loggingprompts + resources only (no tools, logging omitted)
  • full-capstools + prompts + resources + logging: true

Verified each server's advertised capabilities over stdio before connecting:

Server initialize capabilities
no-tools-no-logging { resources, prompts }
full-caps { tools, resources, prompts, logging }

Results

Server Advertised caps Header tabs rendered
no-tools-no-logging resources, prompts Servers · Prompts · Resources · HistoryTools and Logs correctly absent
full-caps tools, resources, prompts, logging Servers · Tools · Prompts · Resources · Logs · HistoryTools and Logs present

The same UI renders Tools/Logs for full-caps but hides them for no-tools-no-logging, proving the tabs are gated on the advertised server capability (the #1516 fix), not on list content or shown unconditionally. (Network is correctly hidden for both — stdio transport.)

Screenshots

1. no-tools-no-logging connected — no Tools tab, no Logs tab (the reported bug, now fixed):

pr1520-01-no-tools-no-logging-tabs

2. full-caps connected — Tools and Logs tabs appear (contrast / control):
pr1520-02-full-caps-tabs

@cliffhall cliffhall merged commit 6d07f6e into v2/main Jun 26, 2026
1 check passed
@cliffhall cliffhall deleted the v2/1516-gate-tabs-on-capabilities branch June 26, 2026 14:34
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.

Gate header tabs/sections on server capabilities (Tools tab shows even when unsupported)

1 participant