Skip to content

McpUiHostCapabilities missing tools field — widget-declared tools unimplementable by hosts #655

@saaage

Description

@saaage

Summary

McpUiAppCapabilities lets a widget declare that it exposes tools (tools?: { listChanged?: boolean }), but McpUiHostCapabilities has no corresponding tools field. This means:

  1. A widget sends tools: {} in ui/initialize to declare it exposes tools
  2. The host has no spec'd way to advertise it will honor that capability
  3. The widget cannot check getHostCapabilities() to know whether the host will ever call bridge.listTools() or route tool calls back through AppBridge

The handshake is one-sided. The widget declares intent; the host has no mechanism to acknowledge it.

Evidence

Confirmed against Claude Desktop (May 2026) via getHostCapabilities():

{
  "openLinks": {},
  "downloadFile": {},
  "serverTools": { "listChanged": true },
  "serverResources": { "listChanged": true },
  "logging": {},
  "updateModelContext": { "text": {}, "image": {} },
  "message": { "text": {} }
}

No tools field. The onlisttools handler registered on the widget never fires. The widget's tools: {} capability declaration is silently ignored.

Current type asymmetry

// Widget side — can declare tool capability ✅
interface McpUiAppCapabilities {
  tools?: { listChanged?: boolean };
}

// Host side — no tools field ❌
interface McpUiHostCapabilities {
  serverTools?: { listChanged?: boolean }; // MCP server → host direction, unrelated
  // no field for: host supports calling widget-declared tools
}

What a fix would require

Spec change: Add tools?: { listChanged?: boolean } to McpUiHostCapabilities:

interface McpUiHostCapabilities {
  // ...existing fields...
  tools?: {
    /** Host will call tools/list on the widget after connect and surface results to the LLM */
    listChanged?: boolean;
  };
}

Host implementation: A host advertising tools: {} would be expected to:

  1. Call bridge.listTools() after the widget connects
  2. Merge widget-declared tools into the LLM's available tool set
  3. Route LLM tool calls matching widget tool names back through bridge to the widget's oncalltool handler
  4. Re-query bridge.listTools() when it receives notifications/tools/list_changed from the widget

Impact

Without this, oncalltool/onlisttools on the widget side are effectively dead code in any real-world deployment. The pattern enables compelling use cases — widgets exposing derived, context-friendly state to the LLM on demand rather than pushing raw state via updateModelContext — but no host can implement it without a spec'd capability to advertise.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions