Overview
MCP Apps is bidirectional. In addition to the View calling tools on the originating MCP server (seecallServerTool), 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
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.
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.
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.
3. Handle tool calls
When the host calls one of the View’s tools, it arrives inoncalltool. Return a CallToolResult with content (for the model) and optional structuredContent (for any UI that reads it).
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 sendsnotifications/tools/list_changed automatically after registerTool(), update(), enable(), disable(), or remove() when the View has initialized with listChanged: true.
sendToolListChanged() when you manage a custom onlisttools result outside the registry:
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
Related
onlisttools— request handler fortools/listoncalltool— request handler fortools/callregisterTool— dynamic registry on theAppsendToolListChanged— notify the host of changes- Tool
_meta— visibility and resource linkage useAppToolsanduseRegisterTool— sunpeak React wrappers