Skip to content
Tools

Page WebMCP Tools in Chat

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.

Set it up

You need a working chat first. If you do not have one, start with the Quick Start.

1. Accept client tools on the server

The server does not know which tools the page has. Let the client send them, and merge them with mergeAgentTools:

ts
// 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.

2. Give the page tools to the chat

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.

tsx
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.

How it works

  1. The page tools API reads the tools from document.modelContext.
  2. It turns each tool into a client tool, with the same name, description, and input schema.
  3. The chat sends these tool definitions to your server. mergeAgentTools gives them to the model.
  4. When the model calls a tool, the chat runs it in the browser through WebMCP.
  5. When the page adds or removes a tool, the list updates.

To send your own client tools to WebMCP, see WebMCP Tools.

Advanced

Skip tools with a filter

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:

  • name
  • origin: the origin of the document that registered the tool.
  • annotations

This filter keeps only read-only tools from your own origin:

tsx
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.

Handle read errors

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.

Duplicate tool names

Tool names must be unique after the filter runs. This includes tools from different frames.

If two tools have the same name:

  • getWebMCPTools() rejects.
  • A subscription sends the error to onError and keeps the last good list.

Give the tools unique names, or use filter to keep only one of them.

Tool results

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.

Read page tools without a framework

Use getWebMCPTools from @tanstack/ai-client to read the tools one time:

ts
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:

ts
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.