If you don’t have an MCP App yet, start with Add an interactive UI to your MCP server.
Register an app tool in a view
A view is one rendered instance of your app’s UI in Claude. To give Claude an action in your app, register a tool in the view’s script, either with a schema library or with your own JSON Schema:- With a schema library: call
app.registerTool - With your own JSON Schema: set the
onlisttoolsandoncalltoolhandlers
app.registerTool before the script calls app.connect(), so that the view declares the tools capability when it connects.
The MCP Apps SDK has two functions with similar names. Use the one that runs in the view:
registerAppTool: call it on your server to register the tool that opens the viewapp.registerTool: call it in the view to register an app tool
add_item, which adds an item to that list.
To run either sample, add <ul id="items"></ul> to the view’s HTML.
Register with app.registerTool and a schema library
Use this option to describe each tool’s input with a schema library, so that you don’t write JSON Schema yourself. Call app.registerTool once for each tool. This option has these requirements:
- Schema library: for the input schema, pass a schema object from a library that implements Standard Schema with JSON Schema output, such as zod 4.2 or later
- SDK version:
app.registerToolrequires@modelcontextprotocol/ext-apps1.7.0 or later - Bundler: the sample imports both packages by name, which a browser can’t resolve, so bundle the view’s script. If your view loads the SDK from a CDN, as in the quickstart, register with handlers.
add_item and writes its input schema with zod:
Register with handlers and your own JSON Schema
Use this option to write each tool’s JSON Schema yourself. Write these pieces:onlisttools: return the tool definitionsoncalltool: run each call. Check the arguments, because the handlers don’t validate them.Appconstructor: pass{ tools: { listChanged: true } }as the second argument
add_item this way:
Test an app tool in Claude
To confirm that Claude can find and call your tool, test it in Claude on the web. Foradd_item from the shopping list samples, follow these steps:
- Add your server to Claude as a custom connector
- Start a new conversation with the unified Claude experience, which combines chat and Cowork. If the message box offers a choice of Chat or Cowork, your account doesn’t have it.
- Set the permission setting in the message box to Manual. In Auto, Claude can use a tool without asking.
- Ask Claude to open the app, and name your connector in the message
- In a second message, ask for a change, such as adding milk to the list.
Check whether your organization has access to app tools
App tools are available to a limited set of organizations, so the account that you test with may not have them. To check your access, follow these steps:- Test a tool in Claude
- If Claude doesn’t ask you to approve the tool and doesn’t run it, rule out the causes under Claude finds no tools in the view
- If none of those causes applies, your organization may not have access. Keep your app working without app tools, and test again later.
Update app tools while a view is open
You can enable, disable, update, remove, or add tools while a view is open. For example, disable an editing tool while no document is open.- With
app.registerTool: keep the handle that the function returns. Callenable(),disable(),update(), orremove()on that handle. - To add a tool after
connect(): callapp.registerToolagain - With the
onlisttoolshandler: callapp.sendToolListChanged()when the list that the handler returns changes.app.sendToolListChangedrequires@modelcontextprotocol/ext-apps1.7.0 or later.
Design app tools
A conversation can hold two views of your app, and each of Claude’s calls reaches only one of them. Write each tool to change only its own view’s state.Approval before an app tool runs
Claude asks the user to approve a tool before the tool runs, unless the user’s saved choice, such as Always allow, or the conversation’s permission setting already applies to the call.- What the prompt names: the connector and the tool’s
title. If you set notitle, the user sees thename. - What else the user can read: the values that Claude is sending, your
description, and the tool’s inputs - If the user declines: the tool isn’t called, and Claude receives an error result that says so
- Always allow: the prompt can offer this option, and Claude can then run that tool on later calls without asking
app.callServerTool, the user doesn’t see the prompt.
Keep each app tool definition constant
A user saves a choice on the approval prompt, such as Always allow, for the tool as you defined it. Claude can ask for approval again after changes such as these:- A change to the tool’s name, title, description, or input schema
- A change to the server tool that opens the view
Write a complete first line for each tool description
Write the first line so that Claude can understand it without the rest of the description. When Claude lists a view’s tools, it receives each tool’s name and the first line of its description, up to 200 characters. Claude receives the rest of the description when it looks up that tool.Return text from app tools
Put what Claude needs in atext block, because Claude reads the text blocks in a result’s content.
structuredContent: Claude reads it, as JSON, only when it receives no text at all fromcontent- Other block types: if you return a block of another type, such as an image, Claude receives text for that block too and doesn’t read the result’s
structuredContent
Report a failure from an app tool
To report a failure, returnisError: true or throw an error. Claude receives an error result, which might not contain your message.
When Claude can recover, for example by changing an input, return a normal result that says what to change.
Limits on app tools
For work that can take longer than the time limit, start the work and return immediately. Register a second tool that reports the outcome.
Views that Claude can reach
If Claude doesn’t call your view’s tools, check the view’s connector, where you test, and whether the view is loaded:- Remote connector: the view must come from a remote connector. Claude doesn’t call the tools of a view from a local server, such as one in the Claude Desktop configuration file.
- Where to test: test on the web, in a conversation with the unified Claude experience.
- On Team and Enterprise plans, an Owner or Primary Owner controls it with the Chat and Cowork unified setting in Organization settings > Capabilities.
- Loaded view: if the view isn’t loaded in the Claude page that the user has open, Claude’s call isn’t sent. Claude receives an error result that says so.
- When a view loads again, it starts from your HTML, so store on your server anything that you need to keep.
Troubleshoot app tools
To see the messages between your view and Claude, open developer tools. The SDK logs these protocol messages withconsole.debug, so in Chrome you see them at the Verbose level.
Claude finds no tools in the view
The view renders and throws no exception, but Claude doesn’t use a tool that you registered. Check for each of these causes:- You asked for the change in the message that opened the app: when the first view opens in a conversation, Claude receives its tools with your next message. Ask Claude to open the app, and name your connector in the message. Then ask for the change in a second message.
- You passed an
inputSchemathat isn’t a schema object:- Cause:
app.registerToolcan’t convert a plain object, such as JSON Schema or the{ name: z.string() }shape that server tools take. - Effect: the view responds to
tools/listwith an error, and Claude can’t see the view’s other tools either. - Fix: pass a schema object from your schema library, or use the
onlisttoolsandoncalltoolhandlers.
- Cause:
- The view exceeds a limit, or Claude can’t reach it: check the limits on app tools and the views that Claude can reach
- Your organization or conversation doesn’t have app tools: check whether your organization has access
app.registerTool is not a function
The installed @modelcontextprotocol/ext-apps package is older than 1.7.0. Upgrade the package, or use the onlisttools and oncalltool handlers.
Client does not support tool capability
On an App that declared no tools capability, the SDK throws this error when you:
- Call
app.registerToolfor the first time afterconnect() - Set
onlisttoolsoroncalltool
(required for tools/call) or (required for tools/list).
To fix the error, register the tool before connect(), or pass { tools: { listChanged: true } } as the second argument to the App constructor.
Schema (vendor: zod) does not implement Standard JSON Schema (~standard.jsonSchema)
- Cause: with
@modelcontextprotocol/ext-apps2.x, the view responds totools/listwith this error when the schema comes from zod 4.1 or earlier - Where it appears: in the view’s console at the Verbose level, in a line that starts with
Sending message - Fix: upgrade zod to 4.2 or later
Next steps
- Send messages to Claude from your MCP App: send Claude a request when the user acts in the view
- Supersede older widget instances: keep one copy of the app active when Claude opens it more than once
- Design guidelines: decide which interactions belong in the app and which in the conversation
- Troubleshoot MCP Apps: fix an app that doesn’t appear or loses its assets