Skip to main content
MCP Apps SDK

Overview

MCP Apps is bidirectional. In addition to the View calling tools on the originating MCP server (see callServerTool), the View itself can act like a tiny MCP server — registering tools that the host or model can call. App-side tools are useful when the answer lives inside the View and can’t be answered by the server:
  • DOM and selection state — get the user’s current selection, scroll position, or visible viewport
  • Form state — submit values from a multi-step wizard or pending edits
  • Local computation — derive values from data the View has already loaded
  • User intent — surface explicit UI actions (a “Save” button, a confirm dialog) to the model
This page ties together the four pieces that make app-side tools work: the tools capability, onlisttools, oncalltool, and list-changed notifications for dynamic tool lists.

Two ways to register tools

The SDK offers two equivalent patterns. Pick the one that matches the shape of your app.

Static list — onlisttools + oncalltool

Declare a fixed list once. The host queries onlisttools, then dispatches calls to oncalltool. Good for apps where the tool set never changes.

Dynamic registry — app.registerTool()

Add, update, enable, disable, or remove tools at runtime. Each call returns a handle with enable(), disable(), remove(), and update(). The SDK builds the tool list, dispatches calls, and sends list-changed notifications automatically when the capability is enabled.
In the sunpeak framework, use useAppTools() for the static pattern or useRegisterTool() for the dynamic pattern.

1. Declare the tools capability

The host only routes tools/list and tools/call to the View if the View declared support during initialization. Pass tools in the capabilities argument to the App constructor.
Set listChanged: true only if you plan to add or remove tools at runtime. The host will re-query onlisttools whenever you send a notifications/tools/list_changed notification. When you call app.registerTool() before connect(), the SDK declares { tools: { listChanged: true } } automatically if you did not provide a tools capability. Manual onlisttools and oncalltool handlers still require you to declare the capability yourself.

2. Declare which tools the app exposes

Static — onlisttools

Return an array of MCP Tool definitions. Each must have a name and an inputSchema (with type: "object").

Dynamic — app.registerTool()

Define tools in code with Standard Schema (Zod, ArkType, Valibot) for input validation. The SDK populates onlisttools and dispatches oncalltool automatically.
The returned handle exposes lifecycle methods:
Register handlers and tools before calling connect() — handlers registered after the initialization handshake will miss the first tools/list call from the host.

3. Handle tool calls

When the host calls one of the View’s tools, it arrives in oncalltool. Return a CallToolResult with content (for the model) and optional structuredContent (for any UI that reads it).
If you use registerTool, you skip this — each tool’s callback runs automatically. The dispatcher checks the registry first, falling back to oncalltool for any unmatched name.

Returning errors

Throw to fail the request at the transport level. Return { isError: true, content: [...] } to surface a tool-level error that the model can see and recover from. Prefer the latter for anything the model could act on.

Dynamic tool lists

The dynamic registry sends notifications/tools/list_changed automatically after registerTool(), update(), enable(), disable(), or remove() when the View has initialized with listChanged: true.
Use sendToolListChanged() when you manage a custom onlisttools result outside the registry:
List-changed notifications work only when the View declared tools: { listChanged: true } and the Host supports app-side tools. The Host then re-runs tools/list and updates the model’s available tools for the next turn.

Tool visibility — model, app, or both

The _meta.ui.visibility array on a tool controls who can call it: "model", "app", or both. App-side tools that are surfaced to the model should remain ["model", "app"] (the default). Set ["app"] for tools that are pure UI plumbing (refresh buttons, pagination) and shouldn’t clutter the model’s tool list. See Tool _meta for the full discussion.

End-to-end example