Your page already has WebMCP tools. A widget, a library, or your own code registered them on document.modelContext. You want your in-page chat to use the same tools, without a second copy of each tool.
The page tools API reads the tools from document.modelContext and gives them to the chat as client tools. When the model calls a tool, the chat runs the tool through WebMCP.
Experimental: WebMCP support is experimental. During SSR, in insecure contexts, or in unsupported browsers, the tool list stays empty.
The server does not know the page tools. Let the client declare 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,
tools: mergeAgentTools([], params.tools),
})
return toServerSentEventsResponse(stream)
}Security: params.tools comes from the browser. The model can call these tools, but the tools run in the browser, not on the server. Keep sensitive work behind your own server checks.
Use 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 })
return (
<button type="button" onClick={() => sendMessage('Open the help panel')}>
Ask ({messages.length} messages)
</button>
)
}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 name, origin, and annotations:
import { usePageWebMCPTools } from '@tanstack/ai-react'
export function useReadOnlyPageTools() {
return usePageWebMCPTools({
filter: (tool) =>
tool.origin === location.origin &&
tool.annotations?.readOnlyHint === true,
})
}Every framework API takes the same filter and onError options. onError gets a failed WebMCP read, for example a NotAllowedError from the tools permissions policy. The last good list stays in place.
Tool names must be unique after filtering, including tools from different frames. If names repeat, getWebMCPTools() rejects and subscriptions report the error through onError. Subscriptions keep the last good list. Give the tools unique names or use filter to select one of them.
Some providers reject a tool name with a period, such as help.open. WebMCP allows periods. Skip those tools with filter.
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'),
tools: await getWebMCPTools(),
})To keep the list current, use subscribeWebMCPTools. It calls your listener now and 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'),
})
const subscription = new AbortController()
subscribeWebMCPTools((tools) => client.updateOptions({ tools }), {
signal: subscription.signal,
filter: (tool) => tool.origin === location.origin,
})
export function stopPageTools() {
subscription.abort()
}Every framework package also exports getWebMCPTools and subscribeWebMCPTools.
Your chat can now call every WebMCP tool on the page that passes your filter. To expose your own client tools to WebMCP, see WebMCP Tools.