MCP App View Tools: App-Provided Tools That Run in the Browser

MCP App view tools let a rendered app expose a small, typed interface back to the model.
The next MCP App search gap is view tools.
Most MCP App docs teach the first loop: the model calls a server tool, the server returns structuredContent, and the host renders a resource. That gets a dashboard, card, picker, map, or form on screen.
The harder question comes after that: how does the model interact with the live UI it just opened?
TL;DR: View tools, also called app-provided tools, let the rendered MCP App register tool handlers from inside the browser iframe. The host can discover them with tools/list and call them with tools/call. Use them for live, browser-side state and commands. Keep durable, privileged, or long-running work on the MCP server.
What View Tools Are
In normal MCP, tools live on the server. The host asks the MCP server for tools/list, the model chooses a tool, and the host calls the server with tools/call.
MCP Apps add a second direction. A rendered app can expose its own tools while the iframe is loaded. The host can list those tools and call them through the same MCP tool messages.
That gives the model a typed interface to the current view.
Without view tools, the app is mostly opaque to the model. The model may know what the server returned, and it may receive pushed state through updateModelContext or useAppState, but it cannot reliably query the app’s current selection, viewport, local calculations, active step, or available UI actions.
With view tools, the app can expose small commands:
get_selected_rowsset_date_rangereset_filtersget_current_viewmove_cursorread_visible_itemsapply_local_sort
Those tools run in the app iframe, against the state the user is actually seeing.
Why This Search Intent Matters
Developers are already searching for terms like:
MCP App view toolsapp provided tools MCP AppsregisterTool MCP App viewmodel call tool inside MCP Appclient side tools MCP AppsMCP App tools/call host to iframe
That intent is different from callServerTool. callServerTool is app to server. View tools are host to app.
It is also different from useAppState. useAppState pushes state from the app to the host. View tools let the model pull state or ask the app to perform an action when needed.
You can think about the three paths this way:
| Need | Pattern |
|---|---|
| UI needs backend work | callServerTool |
| UI needs to tell the model what changed | useAppState or updateModelContext |
| Model needs to query or command the live UI | View tool or app-provided tool |
Good apps often use all three.
Server Tools vs View Tools
The most important design choice is where the tool should live.
| Question | Server tool | View tool |
|---|---|---|
| Where does it run? | MCP server | App iframe |
| How long does it live? | As long as the server connection | As long as the rendered app instance |
| Can it use secrets? | Yes, if the server owns them | No, avoid secrets in browser code |
| Can it write to a database? | Yes | Usually no |
| Can it read live UI state? | Only if the UI sends it | Yes |
| Can it survive iframe teardown? | Yes | No |
| Best for | APIs, databases, auth, durable jobs | Selection, viewport, local state, UI commands |
If the action needs a token, database, file system, billing API, or audit log, keep it on the server.
If the action is about the current view, such as “what item is selected?” or “zoom the chart to this range,” a view tool is a good fit.
A Simple View Tool Shape
The official MCP Apps SDK exposes an App object that can register tools from the app side. The exact wrapper you use may differ by framework, but the protocol shape is the same: declare a tools capability, register a tool name, provide a schema, then return a normal MCP tool result.
import { App } from '@modelcontextprotocol/ext-apps';
import { z } from 'zod';
const app = new App(
{ name: 'InvoiceReview', version: '1.0.0' },
{ tools: { listChanged: true } }
);
app.registerTool(
'invoice_review_get_selection',
{
description: 'Return the invoice rows currently selected in the visible review table.',
inputSchema: z.object({}),
outputSchema: z.object({
selectedIds: z.array(z.string()),
count: z.number(),
}),
annotations: {
readOnlyHint: true,
},
},
async () => {
const selectedIds = getSelectedInvoiceIdsFromViewState();
return {
content: [
{
type: 'text',
text: `${selectedIds.length} invoices are selected.`,
},
],
structuredContent: {
selectedIds,
count: selectedIds.length,
},
};
}
);
The model does not scrape the DOM. It calls invoice_review_get_selection and receives a typed result.
That distinction matters. DOM scraping couples the model to pixels and markup. A view tool gives the model a semantic contract that your app owns.
A Command Tool Example
Read-only state tools are the safest place to start. Command tools need more care because they change what the user sees.
app.registerTool(
'invoice_review_set_filter',
{
description: 'Set the visible invoice status filter in the current invoice review view.',
inputSchema: z.object({
status: z.enum(['all', 'open', 'paid', 'overdue']),
}),
outputSchema: z.object({
status: z.enum(['all', 'open', 'paid', 'overdue']),
visibleCount: z.number(),
}),
annotations: {
readOnlyHint: false,
},
},
async ({ status }) => {
setInvoiceFilter(status);
const visibleCount = countVisibleInvoices();
return {
content: [{ type: 'text', text: `Filter set to ${status}.` }],
structuredContent: { status, visibleCount },
};
}
);
This tool has a side effect, so it should not claim readOnlyHint: true. Hosts can use that annotation when deciding whether to ask the user for approval.
For destructive work, do not use a view tool as the final authority. Let the view collect confirmation, then call a server tool that performs the write with durable auth, logging, and validation.
When to Use useAppState Instead
Use useAppState when the user changes something and the model should know later.
import { useAppState } from 'sunpeak';
const [state, setState] = useAppState({
selectedInvoiceIds: [],
statusFilter: 'all',
});
That is a push model. The app sends the state after a user action. The model can read it on a future turn.
View tools are a pull model. The model asks for state or asks the app to do something.
Use useAppState for committed user choices:
- selected records
- approved steps
- form values
- active workflow state
Use view tools for live questions or commands:
- “Which rows are visible right now?”
- “Move to the next result.”
- “Return the current viewport bounds.”
- “Apply the
overduefilter.” - “Reset this temporary chart state.”
When the state is important for the rest of the conversation, sync it with useAppState after the view tool changes it.
When to Use callServerTool Instead
Use callServerTool when a UI action needs server-side work.
Examples:
- load the next page from an API
- save a draft
- validate a form against backend rules
- start an export job
- write to a database
- refresh data with user credentials
The view can still provide a small tool that lets the model ask for the current UI state, but the actual write should cross back to the server.
import { useCallServerTool } from 'sunpeak';
const callServerTool = useCallServerTool();
async function saveSelectedInvoices(selectedIds: string[]) {
return callServerTool({
name: 'save_invoice_selection',
arguments: { selectedIds },
});
}
A practical pattern is:
- View tool returns the current selection.
- Model explains the proposed write.
- User confirms in the app.
- App calls a server tool through
callServerTool. - Server performs the durable write.
That keeps the view useful without moving trusted work into browser JavaScript.
Capability Detection and Fallbacks
Do not assume every host supports view tools yet.
MCP Apps are cross-host, but host capability support varies. A portable app should detect whether app-provided tools are available. If they are not, the app should still work through user clicks, useAppState, sendMessage, and app-only server tools.
Fallback examples:
| Desired flow | View tool path | Fallback path |
|---|---|---|
| Model asks for current selection | get_selected_rows | Push selection through useAppState on each change |
| Model changes a filter | set_filter | Ask user to click a visible filter control or send a message |
| Model reads viewport bounds | get_current_view | Store bounds with useAppState after pan or zoom |
| Model requests export | prepare_export_view_state, then server write | Button calls an app-only server tool |
Fallbacks matter for ChatGPT Apps, Claude Connectors, and any other MCP App host because support can differ by release, plan, platform, or security policy.
Security Rules for View Tools
View tools run inside a sandboxed iframe. Treat them as untrusted app code from the host’s point of view, and as browser code from your point of view.
Follow these rules:
- Give every view tool a namespaced name, such as
invoice_review_get_selection. - Keep schemas narrow. Avoid arbitrary JSON blobs unless there is no better shape.
- Mark read-only tools with
readOnlyHint: true. - Mark tools with side effects as not read-only.
- Return small, model-readable
contentplus typedstructuredContent. - Never put tokens, secrets, or private implementation data in tool results.
- Add timeouts for handlers that do real work.
- Disable or unregister tools when the relevant UI state is gone.
- Keep writes on the server unless the action is purely local UI state.
Namespacing deserves extra attention. If an app registers a generic tool like search, it can conflict with a server tool or another app tool in the host’s mental model. A name like map_view_search_visible_markers is longer, but it is clearer and safer.
Lifecycle Rules
View tools are tied to the current app instance.
That means:
- They become available only after the app initializes and advertises its tools capability.
- They can change while the app is open.
- The app should notify the host when the tool list changes.
- They disappear when the iframe is torn down.
- A host should return an error if someone calls a view tool after the app is closed.
This is why view tools are a poor fit for long-running work. If the task needs to continue after the user scrolls away, reloads, closes the chat, or the host reclaims the iframe, the task belongs on the server.
How to Test View Tools
Test view tools like a protocol contract, not just a React event.
Start with unit tests for the handler:
- valid input returns the expected
structuredContent - invalid input fails before changing state
- read-only tools do not mutate state
- command tools update the intended state only
isErrorresults are handled by the caller
Then add browser tests against a local host or inspector:
- render the app
- wait for app initialization
- list app-provided tools
- call the view tool with valid arguments
- assert UI changes and tool result data
- call with invalid arguments
- unmount the app
- assert the tool is gone or returns an error
For cross-host testing, add the fallback path to the same matrix. The app should still be usable when view tools are unavailable.
This is where sunpeak helps. Use the sunpeak inspector and testing framework to pin the resource state, run the browser interaction, and verify the fallback behavior before you try it in a live host. If a host does not support app-provided tools yet, keep the test focused on useAppState, callServerTool, and the visible UI controls.
A Practical Checklist
Before you add a view tool, answer these questions:
- Does the model need to pull this state, or can the app push it with
useAppState? - Does this action need secrets, durable storage, or an external API?
- What happens if the iframe is destroyed halfway through?
- Is the tool name scoped to the app and current view?
- Is the schema tight enough to validate before the handler runs?
- Should the host ask for user approval?
- What is the fallback when the host does not support view tools?
- Can this be tested locally without a real host account?
If the answer points to live UI state, a view tool is a good fit. If the answer points to durable work, use a server tool.
What to Build First
Start with one read-only view tool. Do not begin with a write action.
A good first tool is get_current_selection, get_visible_items, or get_view_state. It teaches the host and model what the app can expose without changing user data. Once that works, add one command tool with a narrow schema and a clear fallback.
The best MCP Apps keep the boundary simple: the view owns live interaction, the server owns durable work, and the model gets typed tools instead of guessing from pixels.
When you are ready to test that boundary, run the app in sunpeak, exercise the bridge locally, and add a cross-host fallback test. The first time a host changes tool support, you want a failing test, not a production bug.
Get Started
npx sunpeak newFurther Reading
- MCP App actions - callServerTool, sendMessage, and updateModelContext
- Interactive MCP Apps with useAppState - two-way UI state patterns
- MCP App lifecycle - connect, tool input, results, and teardown
- MCP App tool metadata - resourceUri, visibility, and app-only tools
- Testing MCP App data flow - content, structuredContent, _meta, and host bridge state
- Cross-host compatibility testing for MCP Apps
- MCP App framework
- sunpeak docs - MCP Apps introduction
- Official MCP Apps overview
Frequently Asked Questions
What are MCP App view tools?
MCP App view tools are tools registered by the rendered app view instead of the MCP server. The MCP Apps draft spec calls them app-provided tools. They let the host and model call typed browser-side handlers through standard MCP tools/list and tools/call messages while the app iframe is loaded.
What is the difference between a view tool and a server tool?
A server tool lives on the MCP server and is available whenever the server is connected. A view tool lives inside the app iframe, runs in browser JavaScript, and disappears when the iframe is torn down. Use server tools for durable work, privileged API calls, database writes, and long-running jobs. Use view tools for live UI state, local interactions, and semantic commands against the current app instance.
Should I use useAppState or a view tool?
Use useAppState when the app should push state to the model after a user action. Use a view tool when the model should pull state or ask the app to perform an action on demand. Many interactive apps use both: useAppState for committed user choices, and view tools for commands such as get current selection, move to next item, reset filters, or read current viewport.
Do view tools replace callServerTool?
No. callServerTool sends work from the app back to the MCP server through the host bridge. View tools go the other direction: the host calls a tool implemented inside the app view. If the work needs secrets, durable storage, external APIs, or a job that outlives the iframe, keep it on the server and call it with callServerTool.
Are view tools safe?
They can be safe when hosts enforce approval, timeouts, result validation, clear attribution, and lifecycle limits. View tools run in a sandboxed iframe, so hosts should treat them as app code rather than trusted server code. Side-effecting view tools should use clear names, tight schemas, and readOnlyHint false so the host can ask for user approval.
How do I test MCP App view tools?
Test the app tool registry, input validation, handler behavior, result schema, lifecycle cleanup, and fallback path. Add browser tests that render the app, call the registered tool through the host bridge, assert the returned structuredContent, then unmount the iframe and verify the tool is no longer callable.
Can ChatGPT Apps and Claude Connectors use view tools?
View tool support depends on the host and framework version. Build a capability check and fallback into portable MCP Apps. If a target host does not support app-provided tools yet, use useAppState, sendMessage, or app-only server tools through callServerTool to provide the same user workflow.