Read the WebMCP tools already registered on document.modelContext and pass those same tools to the in-page chat.
The chat gets them as client tools. When the model calls one, the chat runs it through WebMCP.
Warning
WebMCP is experimental. During SSR, in an insecure context, or in a browser without WebMCP, the tool list stays empty.
You need a working chat first. If you do not have one, start with the Quick Start.
The server does not know which tools the page has. Let the client send them, and merge them with mergeAgentTools:
// api/chat.ts
import {
chat,
chatParamsFromRequest,
mergeAgentTools,
toServerSentEventsResponse,
} from '@tanstack/ai'
import { openaiText } from '@tanstack/ai-openai'
export async function POST(request: Request) {
const params = await chatParamsFromRequest(request)
const stream = chat({
adapter: openaiText('gpt-6-astra'),
messages: params.messages,
threadId: params.threadId,
tools: mergeAgentTools([], params.tools),
})
return toServerSentEventsResponse(stream)
}Warning
params.tools comes from the browser. The model can call these tools, but they run in the browser, not on the server. Keep sensitive work behind your own server checks.
Call the page tools API for your framework. Pass the result to the chat as tools.
usePageWebMCPTools returns an array. The array updates when the page adds or removes a tool.
import {
fetchServerSentEvents,
useChat,
usePageWebMCPTools,
} from '@tanstack/ai-react'
const connection = fetchServerSentEvents('/api/chat')
export function Chat() {
const pageTools = usePageWebMCPTools()
const { messages, sendMessage } = useChat({ connection, tools: pageTools, threadId: 'support-chat' })
return (
<button type="button" onClick={() => sendMessage('Open the help panel')}>
Ask ({messages.length} messages)
</button>
)
}That is all. Your chat can now call every WebMCP tool on the page. Ask it to "Open the help panel", and the model calls the page tool that does it.
To send your own client tools to WebMCP, see WebMCP Tools.
Pass filter to skip a tool. Return false, and the chat does not get that tool. The filter gets the WebMCP tool, so you can read these fields:
This filter keeps only read-only tools from your own origin:
import { usePageWebMCPTools } from '@tanstack/ai-react'
export function useReadOnlyPageTools() {
return usePageWebMCPTools({
filter: (tool) =>
tool.origin === location.origin &&
tool.annotations?.readOnlyHint === true,
})
}Some providers reject a tool name with a period, such as help.open. WebMCP allows periods. Use filter to skip those tools.
Every framework API takes the same two options: filter and onError.
onError gets a failed WebMCP read. An example is a NotAllowedError from the tools permissions policy. When a read fails, the last good list stays in place.
Tool names must be unique after the filter runs. This includes tools from different frames.
If two tools have the same name:
Give the tools unique names, or use filter to keep only one of them.
WebMCP returns each tool result as a string. If the string is valid JSON, the chat parses it. If not, the chat gets the string as it is.
Use getWebMCPTools from @tanstack/ai-client to read the tools one time:
import { ChatClient, fetchServerSentEvents, getWebMCPTools } from '@tanstack/ai-client'
const client = new ChatClient({
connection: fetchServerSentEvents('/api/chat'),
threadId: 'support-chat',
tools: await getWebMCPTools(),
})To keep the list current, use subscribeWebMCPTools. It calls your listener now, and again after each WebMCP toolchange event. Abort the signal to stop:
import {
ChatClient,
fetchServerSentEvents,
subscribeWebMCPTools,
} from '@tanstack/ai-client'
const client = new ChatClient({
connection: fetchServerSentEvents('/api/chat'),
threadId: 'support-chat',
})
const subscription = new AbortController()
subscribeWebMCPTools((tools) => client.updateOptions({ tools }), {
signal: subscription.signal,
filter: (tool) => tool.origin === location.origin,
})
export function stopPageTools() {
subscription.abort()
}If WebMCP is not available, subscribeWebMCPTools calls your listener one time with an empty array.
Every framework package also exports getWebMCPTools and subscribeWebMCPTools.