How to expose WebMCP tools on your site
A practical guide to declaring WebMCP tools: what to expose, how to write descriptions an agent can actually choose between, and the security boundary people forget.
Max Kruger
WebMCP lets your page hand an agent a set of typed tools instead of making it infer what your interface does. This is how to declare them without creating problems for yourself.
Accurate as of July 2026. WebMCP is a Chrome origin trial and the API can still change.
Register early
Tools are declared through navigator.modelContext:
navigator.modelContext.provideContext({
tools: [
{
name: 'searchProducts',
description: 'Search the product catalogue by keyword, with optional '
+ 'category and maximum price filters. Returns up to 20 matches '
+ 'with name, price and availability.',
inputSchema: {
type: 'object',
properties: {
query: { type: 'string', description: 'Search keywords' },
category: { type: 'string', description: 'Optional category slug' },
maxPrice: { type: 'number', description: 'Maximum price in GBP' }
},
required: ['query']
},
async execute({ query, category, maxPrice }) {
const results = await storefront.search({ query, category, maxPrice })
return { content: [{ type: 'text', text: JSON.stringify(results) }] }
}
}
]
})
Check the entry point before anything else. The API this hangs off was renamed during incubation, and guides written earlier in 2026 show the old one. Code against the old name does not throw. It registers nothing, and your page looks to an agent exactly like a page with no tools.
Register as early as you reasonably can. An agent that inspects the page before your late-firing callback runs sees an empty toolset.
Descriptions decide whether you get used
The schema is a contract. The description is the sales pitch, and it is what actually determines whether an agent picks your tool.
Picture a model holding six tools and a user request. It reads the descriptions and chooses. Anything vague loses.
// Useless. Search what? Returning what?
description: 'Search'
// Barely better. Still no idea what comes back.
description: 'Searches products'
// Useful. Scope, inputs, and what to expect back.
description: 'Search the product catalogue by keyword, with optional category '
+ 'and maximum price filters. Returns up to 20 matches with name, '
+ 'price and availability.'
Write them for a competent new colleague who has never seen your product. Say what it does, what it needs, what it returns, and any limit worth knowing.
The same applies to parameters. { type: 'string' } tells a model nothing about what belongs there. A one-line description on each property is cheap and prevents a whole category of malformed calls.
Choosing what to expose
Start with operations that are high value and hard to infer, and be more careful as you move toward writes.
Good first candidates. Catalogue search, filtering by attributes, stock lookup for a variant, delivery options for a postcode, order status by reference. These are read-only, genuinely awkward to drive through a UI, and low consequence when a call is wrong.
Reasonable with care. Add to basket, apply a discount code, update a quantity. Real value, and reversible.
Think hard. Placing an order, changing a delivery address, cancelling something, anything touching stored payment details. A tool description is an invitation, and the agent calling it may be acting on a loosely worded instruction from a person who has stopped watching.
Do not expose. Anything you would not put behind a public API.
The security point people miss
A declared tool is a documented, machine-readable, discoverable entry point into your application.
WebMCP adds no security boundary. It makes your existing ones easier to find. Everything you already believe about authentication, authorisation, rate limiting and validation applies unchanged, and applies harder, because you have just published a schema explaining exactly how to call you.
Three concrete rules:
Authorise inside execute, every time. Never rely on the tool not being advertised. A tool that reads an order must confirm the current session owns that order.
async execute({ orderId }) {
const order = await api.orders.find(orderId)
if (!order || order.customerId !== session.customerId) {
return { content: [{ type: 'text', text: 'Order not found.' }] }
}
return { content: [{ type: 'text', text: JSON.stringify(order) }] }
}
Validate against the schema, do not trust it. The schema tells a well-behaved agent what to send. It does not stop anything from sending something else.
Rate limit tool calls. An agent in a loop can call a tool far faster than a person can click.
Return text a model can use
Tool results come back as content the model reads. Two failure modes are common: returning too much, and returning something unreadable.
// Dumping a raw API response wastes context and buries the answer.
return { content: [{ type: 'text', text: JSON.stringify(fullApiResponse) }] }
// Return the fields that matter, shaped for reading.
return { content: [{ type: 'text', text: JSON.stringify(
results.slice(0, 20).map(p => ({
name: p.title,
price: `£${p.price}`,
inStock: p.availableForSale,
url: p.onlineStoreUrl,
}))
) }] }
Include a URL where it makes sense. It lets the agent hand the person a link rather than only a description.
Handle the empty case explicitly. [] is ambiguous; "No products matched that search" is not.
Keep the toolset small
The temptation is to expose everything. Resist it, for two reasons.
Every tool consumes context and adds a choice the model can get wrong. Twenty tools with overlapping descriptions produce worse behaviour than five clear ones.
And every tool is surface area you now maintain, secure and keep honest as your application changes.
Start with three or four that cover your most common tasks. Add more when you can point at something an agent could not do.
Verify in a browser, not in your source
Because registration happens in page script, you cannot confirm your tools by reading your HTML. A static scan of your markup tells you nothing about what was actually registered at runtime.
The tools have to be evaluated in a real browser, which is why our WebMCP checker runs a browser tier rather than pattern matching. Once you have tools declared, check what an agent would actually receive rather than what you believe you registered.
For the same reason, this belongs in CI. Covered in testing WebMCP contracts in CI.
Checklist
- Confirm you are calling the current entry point, not the renamed one
- Register early in page lifecycle
- Three to five tools to begin with, not twenty
- Descriptions that state scope, inputs and what comes back
- A description on every schema property
- Authorisation inside every
execute, never implied by advertisement - Server-side validation regardless of schema
- Trimmed, readable results with an explicit empty case
- Verified in a real browser, then in CI
Read next
- What is WebMCP? — the model and where the trial stands
- Testing WebMCP contracts in CI — catching a silent rename before your users do
- How agents see your website — what happens without declared tools