Copied to Clipboard
pagehide', () => pageController.abort(), { once: true });
navigator.modelContext.registerTool({
name: 'yubhub_fetch_job',
description: 'Fetch a single YubHub job as clean markdown (company, location, salary, work arrangement, full description, apply URL, skills). More token-efficient than the HTML page.',
inputSchema: {
type: 'object',
properties: {
id: { type: 'string', pattern: '^job_[a-z0-9_-]+$' },
},
required: ['id'],
},
execute: async (params) => {
const r = await fetch('/jobs/' + params.id + '.md',
{ headers: { Accept: 'text/markdown' } });
return { id: params.id, markdown: await r.text() };
},
}, { signal: pageController.signal });
There are 4 things to look at here – firstly, the feature-detection guard – navigator.modelContext only exists in Chrome 146 with the flag on, so every public-facing scaffold needs to short-circuit gracefully elsewhere or you’ll throw on every other browser your users visit from.
The pattern constraint on id – the spec treats JSON Schema loosely in the browser, so validating in code catches the cases the schema doesn’t. And the return value is a structured object, not a string – the agent reads it as data rather than trying to parse text/html.
Model Context Tool Inspector panel showing the tool definition highlighted in a green box and the agent prompt highlighted in a red box
The Inspector showing the tool’s schema (green box) and the agent’s prompt (red box). You can drive this entirely by hand during development, which is the fastest way to catch bad tool descriptions.
The Declarative API Is Free Engineering
I think this is the cleverest part of the WebMCP spec. If you’ve got an HTML form – and of course, you do, you can annotate it and get a tool registration for free.
YubHub’s nav already had a GET form submitting to /jobs/search. All I did was add three attributes:
<form action="/jobs/search" method="GET"
toolname="yubhub_search"
tooldescription="Search YubHub's enriched job board by free-text query - title, company, or keyword. Submits a GET to /jobs/search and renders the results page."
toolautosubmit>
<input type="search" name="q"
toolparamdescription="Job title, company name, or any keyword (2-100 chars)"
minlength="2" maxlength="100" />
</form>
The browser reads the form, builds a JSON Schema from the input fields, and registers it as a tool. When an agent calls it with {q: "python"}, the browser focuses the form, populates the input, and – because of toolautosubmit – submits it automatically. Zero JavaScript. Your form already works for humans; now it works for agents.
One thing worth flagging: toolautosubmit is only really safe on read-only operations. Search queries, availability lookups, status checks – fine, let the agent submit. Anything that creates, modifies, or deletes data should leave the flag off, so the human has to click Submit after the agent fills the form.
And that, I reckon, is how WebMCP adoption will probably start to emerge – by adding it to their internal site search forms and results pages. But I hope to see people agreeing that this idea undersells what WebMCP can do in the longer term.
Context-Scoped Tool Registration
I’ve got a tool called yubhub_current_job. It takes no arguments, and it just returns the current job’s markdown. Obviously, that only makes sense on my job listing pages, so anywhere else on the site it would be meaningless (worse, it’d be confusing). The scaffold pattern-matches the URL and registers conditionally:
const jobMatch = location.pathname.match(/^\/jobs\/(job_[a-z0-9_-]+)$/i);
if (jobMatch) {
const currentJobId = jobMatch[1];
navigator.modelContext.registerTool({
name: 'yubhub_current_job',
description: 'Return clean markdown for the job the user is currently viewing ('
+ currentJobId + '). Zero-arg shortcut.',
inputSchema: { type: 'object', properties: {} },
execute: async () => {
const r = await fetch('/jobs/' + currentJobId + '.md');
return { id: currentJobId, markdown: await r.text() };
},
}, { signal: pageController.signal });
}
The AbortController ties it to the page lifecycle – when the user navigates away, pagehide fires, the controller aborts, and all tools registered with its signal drop off the agent’s radar. Importantly this leaves no leftover closures holding references to the previous page’s state.
Diagram showing three YubHub pages (homepage, job detail, facet archive) with different sets of registered WebMCP tools on each
Same site, three pages, three different tool menus. The agent sees only the tools that make contextual sense.
Navigation Tools vs Data-Retrieval Tools
There are two kinds of tools you can give an agent, and they create completely different user experiences.
Navigation tools change the URL on the user’s behalf. They’re the right call when the user actually wants to end up somewhere else – looking at a specific job, browsing a facet, running a search. So the agent calls yubhub_browse_jobs({type: 'skill', slug: 'figma'}), the tab navigates, the user sees the archive page. The return value is basically a receipt ("I took you here, here’s what’s on the page").
The real power, IMO, is this: Data-retrieval tools return parsed data and leave the URL alone. The agent calls them to reason, compare, filter, summarise – and the user stays on whatever page they were already on, quite unaware that the agent just read a hundred jobs to answer a question. yubhub_fetch_facet returns up to 100 schema.org JobPosting entries; yubhub_shortlist filters them by salary floor, remote status, title keyword, and hands back a ranked list.
Data-retrieval is what I’m bullish on – it’s a midbendingly powerful solution to the current state of play with AI based webcrawl / actions for users and so on.
Inspector panel showing a ranked list of product manager job results returned by a WebMCP data-retrieval tool
yubhub_shortlist returning a ranked list of product manager roles.
Only shipping navigation tools is the mistake I’d warn you off. It just makes the agent a fancy URL-rewriter – it can answer "take me to X", but it can’t answer "summarise the options and recommend one" without hauling the whole user along.
What WebMCP Can’t Do (Yet)
A browsing context is required. There’s no headless mode – if a tab isn’t open, the tools don’t exist. That rules out batch or scheduled agentic work against a WebMCP site, for now.
Discoverability is thin. An agent has to visit your page to see your tools. There’s no global registry, no "hey agent, here’s a list of sites with useful WebMCP tools" index. A .well-known/webmcp manifest has been proposed – and I think it’ll be necessary eventually – but nothing’s shipped yet. Someone is going to come up with an authority directory of WebMCP supportive brands any minute now.
The spec moves weekly. Between my first discovery of the docs and shipping, my implementation, provideContext and clearContext got removed entirely, unregisterTool came back after being briefly deprecated, and the AbortSignal handling in registerTool‘s second argument settled into the form above. Track the spec repo and expect to rewrite things. I’m in the preview group which is very easy to register for.
If you run a site that has structured data behind predictable URLs – a product catalogue, a job board, a docs site, a media library – WebMCP is worth shipping now, even though browser adoption is near zero. It’s cheap to add, it forces you to think about your tool contracts before agents try to guess them, and it positions the site for when Chrome lifts the flag and Edge follows. When that happens I think there will be quite a fuss about it.
The post Implementing WebMCP on a Recruitment Website appeared first on Houtini.