WebMCP lets a page register tools for an AI agent in the browser.
MCP gives remote agents access to your server. WebMCP gives the agent on your page named functions for searching content, filling forms and working with UI state. Your functions define which actions the agent can request.
document.modelContext is unavailable.Site Tools
Turn on webmcp to register three read-only tools for your indexed content:
export default defineNuxtConfig({
aiReady: {
webmcp: true,
},
})The site tools read stored pages, so they turn on the runtime database.
The database needs drizzle-orm, an optional peer dependency: npx nypm add drizzle-orm@1.0.0-rc.4.
If it is missing, the build fails and names the install command.
| Tool | Description |
|---|---|
list_pages | Routes, titles and descriptions, with paging |
search_pages | Searches indexed content, with a metadata fallback |
get_page_markdown | Reads a route as markdown |
On a server, search_pages searches stored page content.
The static fallback searches /__ai-ready/pages.json by route, title, headings, keywords, and description. It does not search Markdown bodies.
The fallback also runs when the server returns no results.
These tools read the same page index as the MCP server. Prerender your pages or enable runtime indexing before using them. The /__ai-ready/pages endpoint exposes indexed page data publicly.
Only index content you intend to publish; the endpoint does not check your page-level access controls.
Nuxt AI Ready marks all three tools with readOnlyHint and untrustedContentHint. The annotations tell the agent that the tools do not change state and that page content may contain unsafe instructions.
Tool settings live under aiReady.tools because the same definitions can run through MCP Toolkit and WebMCP. Each tool has separate attachment settings:
export default defineNuxtConfig({
aiReady: {
tools: {
listPages: {
webmcp: { enabled: false },
},
searchPages: {
defaultLimit: 20,
webmcp: { maxOutputChars: 3000 },
},
},
webmcp: true,
},
})This keeps list_pages on the regular MCP server but leaves it out of WebMCP. defaultLimit applies to both transports.
Your Own Tools
useWebMcpTool() registers a tool for the lifetime of the current component. Turn on webmcp to auto-import the composable, with or without the built-in site tools.
<script setup lang="ts">
const maxPrice = ref(Infinity)
const canFilter = ref(true)
const registration = useWebMcpTool({
name: 'filter_products',
title: 'Filter products',
description: 'Filters the visible product list by a maximum price.',
inputSchema: {
type: 'object',
properties: {
maxPrice: { type: 'number', description: 'Highest price to show, in dollars.' },
},
required: ['maxPrice'],
},
execute: ({ maxPrice: price }) => {
maxPrice.value = price
return `Showing products under $${price}.`
},
}, {
enabled: canFilter,
})
watchEffect(() => {
if (registration.state.value._tag === 'Failed')
console.error('WebMCP registration failed', registration.state.value.error)
})
</script>The schema determines the execute input type.
This example changes UI state, so it does not set readOnlyHint. The composable removes the tool when its component unmounts or a cached component deactivates, then restores it when the component activates. Pass a ref or getter as enabled when availability depends on local state.
The returned supported ref stays false through server rendering and the first client render, avoiding hydration differences. state reports Unsupported, Inactive, Registering, Registered or Failed. unregister() permanently removes this composable instance.
Keep the active tool set small. Every definition uses agent context. See Chrome's tool design guidance.
An execute function can return any serializable value. Return an explicit result for expected failures:
execute: async ({ sku }) => {
const added = await cart.add(sku)
if (!added) {
return {
ok: false,
reason: `${sku} is out of stock.`,
nextTool: 'search_products',
}
}
return { ok: true, message: `Added ${sku} to the cart.` }
}For an expected failure, return a reason and a useful next action. Keep schemas broad enough for harmless variations, then normalize input in execute. A route tool, for example, can accept both about and /about.
Use useWebMcpSupported() when you want to show agent-specific UI:
const showAgentHints = useWebMcpSupported()Customizing Built-in Tools
The ai-ready:webmcp:tools runtime hook runs before registration. You can add, remove or replace definitions, then change shared or per-tool registration options:
export default defineNuxtPlugin((nuxtApp) => {
nuxtApp.hook('ai-ready:webmcp:tools', ({ tools, registerOptions, toolRegisterOptions }) => {
const listPages = tools.findIndex(tool => tool.name === 'list_pages')
if (listPages !== -1)
tools.splice(listPages, 1)
registerOptions.exposedTo = ['https://agent.example.com']
toolRegisterOptions.search_pages = {
exposedTo: ['https://search-agent.example.com'],
}
})
})Nuxt generates the hook type when you enable WebMCP. nuxt-ai-ready/webmcp also exports createSiteTools(), defineWebMcpTool() and the WebMCP types.
Security
By default, other websites and cross-origin frames cannot access your tools.
Use exposedTo to grant access across an embedding relationship with a trusted origin:
export default defineNuxtConfig({
aiReady: {
webmcp: {
exposedTo: ['https://partner.example.com'],
},
},
})This is the default for built-in tools and tools registered by useWebMcpTool(). Override it for one tool through the composable options:
useWebMcpTool(tool, {
exposedTo: ['https://specialist.example.com'],
})Built-in tools use the same per-tool override:
export default defineNuxtConfig({
aiReady: {
tools: {
searchPages: {
webmcp: {
exposedTo: ['https://specialist.example.com'],
},
},
},
webmcp: true,
},
})A listed origin can access the tool when either site embeds the other. This does not create a remote server endpoint or replace application authorization. Follow Chrome's security guidance before sharing tools that use account data.
Page content can also contain prompt injection aimed at the agent. Mark tools that return user-generated or third-party content with untrustedContentHint. Only set readOnlyHint when a tool cannot change state.
Built-in tools cap output at 1,500 characters and include a link to the full source. Change the limit if your pages need more:
export default defineNuxtConfig({
aiReady: {
tools: {
getPageMarkdown: {
webmcp: { maxOutputChars: 4000 },
},
},
webmcp: true,
},
})Chrome recommends tool names of 30 characters or fewer, descriptions of 500 or fewer and parameter descriptions of 150 or fewer. useWebMcpTool() warns in development when your tool exceeds one of these budgets.
Testing
Tool selection varies between model runs. Unit tests can verify registration and handlers, but they cannot prove that an agent will choose the right tool. Use Chrome's evals guide to test tool selection, arguments and multi-step tasks. The Model Context Tool Inspector extension shows which tools a page registered.