Framework Guide · WebMCP · Express
WebMCP with Express: turning a server-rendered site into an agent’s toolset
Express changes the WebMCP conversation entirely: no islands, no hydration boundary — a request, a middleware chain, a template, and a browser that receives finished markup. That makes the integration trivially simple and quietly dangerous in equal measure, because in a server-rendered app the temptation is to treat the process that answers requests as if it hosted the tools. This article, written for backend engineers, covers the template-delivered registration pattern, the declarative form pass every Express shop should schedule, and the security posture of tools that run with the visitor’s session.
Fact-checked and updated on September 2, 2026 (Express 5.x documentation). Primary sources are cited at the end of the article.
The fact that reorganizes the architecture
Re-read the specification’s boundary conditions and the constraint is blunt: tools register in a live browser tab — secure context, top-level document, origin-isolated, never in iframes or workers ([W3C WebMCP specification](https://webmachinelearning.github.io/webmcp)). An Express route handler has no document. You cannot register a WebMCP tool from app.get(), and no amount of server cleverness changes that.
What you can do — and what a mature Express app does well — is treat templates as the delivery vehicle for a small, explicit client runtime: one include that declares, per page, which actions agents may call. Everything business stays exactly where it is today, behind your routers. The Express developer’s instincts (middleware chains, explicit contracts per route) map almost one-to-one onto this; if anything, server-rendered stacks adopt WebMCP more cleanly than SPAs because the decision — which tools live on which page — is already made, in code you review.
Implementation status: what answers today
Chrome’s rollout defines your window: an early preview behind chrome://flags/#enable-webmcp-testing since Chrome 146 (February 2026), and a public origin trial from Chrome 149 through 156 (starting June 2026) that exposes the API on your registered origin without flags ([Chrome for Developers](https://developer.chrome.com/docs/ai/webmcp)). In Express terms, delivering the trial token is a one-liner in your middleware chain — which is the entire point of the framework:
app.use((req, res, next) => {
if (process.env.WEBMCP_OT_TOKEN) {
res.setHeader('Origin-Trial', process.env.WEBMCP_OT_TOKEN);
}
next();
});
Edge, Firefox and Safari have not shipped the API; desktop MCP clients (Claude Desktop, Cursor, Windsurf) do not render pages and therefore cannot see in-browser tools at all. For that traffic, the Express answer is a second surface with the same contract — an always-on MCP endpoint — discussed below.
The pattern: a webmcp partial that the router feeds
One include, every page. The route handler decides which tools exist on the response and seeds them with fresh server data; the browser executes them:
// routes/products.js
router.get('/products/:id', async (req, res, next) => {
const product = await db.product.find(req.params.id);
if (!product) return next();
res.render('product', {
product,
webmcpData: { id: product.id, stock: product.stock, currency: product.currency },
webmcpTools: ['check-stock', 'join-waitlist'],
});
});
<%-- views/partials/webmcp.ejs --%>
<script type="application/json" id="webmcp-data"><%- JSON.stringify(webmcpData || {}) %></script>
<script>
(function () {
var ctx = document.modelContext || navigator.modelContext;
if (!ctx || !('registerTool' in ctx)) return;
var data = JSON.parse(document.getElementById('webmcp-data').textContent || '{}');
var controller = new AbortController();
(window.__WEBMCP_TOOLS__ || []).forEach(function (tool) {
ctx.registerTool(tool(data), { signal: controller.signal });
});
})();
</script>
Per-page tool modules receive the seeded data at registration time — check-stock answers from data.stock in microseconds, no round-trip — and a product that is out of stock simply never receives the join-waitlist registration: the webmcpTools array, computed in your route, is the permission surface. Tool availability inheriting route logic is the middleware mindset applied to agents, and it is worth more than it first appears: no second authorization layer exists to drift out of sync with the first.
The registration itself follows the specification’s mechanics exactly: the content envelope is MCP’s { content: [{ type: "text", text }] } shape, the AbortController signal is the only deregistration mechanism (the spec removed unregisterTool()), and feature detection on document.modelContext || navigator.modelContext survives the root-object migration Chrome’s own guidance documents ([GoogleChrome modern-web-guidance](https://github.com/GoogleChrome/modern-web-guidance/blob/main/skills/modern-web-guidance/guides/webmcp/agentic-javascript-tools.md)).
The declarative pass: three attributes, zero JavaScript, biggest win
Express apps render more forms than anything else — search, contact, booking, wishlist. The Declarative API, merged into the specification as the form-level surface of WebMCP ([explainer](https://github.com/webmachinelearning/webmcp/blob/main/declarative-api-explainer.md)), turns any of them into a callable tool with markup alone:
<form action="/search" method="get"
toolname="site-search"
tooldescription="Searches the Acme catalog. Returns a results page listing up to 20 products for the query.">
<input type="search" name="q" required placeholder="Search" />
<select name="cat">
<option value="all">All departments</option>
<option value="shirts">Shirts</option>
<option value="outerwear">Outerwear</option>
</select>
<button type="submit">Search</button>
</form>
The browser synthesizes the input schema deterministically from the controls — q as required string, cat as a string enum over the option values — per the mapping rules dissected in our Declarative API reference. Omit toolautosubmit and the human keeps final control of submission, with the :tool-form-active pseudo-class available to style agent-filled forms transparently. A one-hour pass annotating every form in views/ — each template line you touch already routes through the same routers your tests cover — is legitimately the highest ROI agent-readiness work available anywhere in this series.
<select>s or numeric min/max: schema synthesis respects form constraints, so constrain them server-side where you already validate.The honest constraint: endpoints are forever, tools live per-tab
Any Express developer asks it correctly on day two: “my endpoints are always up; my tools are up only while a human or agent holds a page open?” Yes — and the comparison is a real architectural decision, not a defect to complain about:
| Dimension | Express REST endpoint | WebMCP tool |
|---|---|---|
| Lifetime | Always, independent of any page | Only while the page is open |
| Clients | Every HTTP consumer (curl, Postman, LLMs) | Chrome 149+ with trial token |
| Auth | Session, JWT, API keys | Inherits page context and user’s cookie |
| Schema | Written by hand (OpenAPI/Swagger) | Declared at registration |
| Failure | HTTP 4xx/5xx | Caught in the browser, returned to the agent |
If your agent traffic is headless — ingestion pipelines, partner integrations — it needs a real MCP server: an always-on /mcp HTTP endpoint speaking tools/list and tools/call ([comparison with runnable examples](https://mcptrail.com/blog/how-to-add-webmcp-to-a-website/)). The architecture that survives review keeps one tool definition per capability — name, description, JSON Schema — rendered into both the Express-side MCP endpoint and the browser-side registration. Two hand-maintained lists drift; one definition cannot. (This is exactly the spine of our WordPress flagship guide, where the same registry feeds wp-json and the browser.)
Security for the backend mind
- A tool’s
executeruns as the logged-in user. No extra auth, by design — which means a prompt-injected agent riding a real session can call anything registered. Validate insideexecuteexactly as you validatereq.body: tool parameters are user input wearing nicer clothes ([Express security best practices](https://expressjs.com/en/advanced/best-practice-security.html)). - Never interpolate tool arguments into queries, shell calls or view paths. The classic Express footguns — unvalidated
reqreachingres.renderor a raw SQL string — apply with a new, faster attacker. - Route write-tools through named server endpoints. Abuse lands in your access logs regardless; named paths keep it legible. Log server-side, audit as you would any untrusted client.
- Mark read-only tools
annotations: { readOnlyHint: true }— today advisory, tomorrow the likely hook for browser-side auto-approval policy ([Chrome agent security](https://developer.chrome.com/docs/agents/security)).
Verification: supertest and the console
Local: flag on, rendered page, DevTools — (await document.modelContext.getTools()).map(t => t.name) must equal the route’s webmcpTools array; that invariant is unit-testable without a browser, by rendering the view under supertest and asserting the payload. Live agent-call path: executeTool('site-search', '{"q":"merino"}'), then production with the trial header via the Origin Trials console registration.
FAQ
Can a tool call a route handler directly instead of over HTTP?
No — the browser cannot reach the Express process. execute calls the same HTTP endpoint a human-triggered form would; treat it as another client of your public interface, with the visitor’s credentials.
Is toolautosubmit ever right for checkout?
No. The Declarative API defaults to human-confirmed submission precisely for consequential actions; autosubmit belongs on searches and filters. The rationale is spelled out in our Declarative API article.
We use Pug, not EJS.
Unaffected — every pattern here is an attribute and an include. WebMCP speaks HTML, not template engines.
Does Express 5 change anything relevant?
Two things: removed deprecated middleware patterns mean fewer legacy helpers around req.body, and async-error handling means a rejecting execute path behaves like any other error route — your standard error middleware remains the last line.
Conclusion
Express will never feel agentic, and it does not need to. Its routers were always the contract between state and the client; WebMCP adds one more consumer of that contract, declared in the same templates that render it. Annotate the forms, ship the partial, gate the writes behind the endpoints that already enforce them, and your decade-old server-rendered app is suddenly first-class citizen software for the browser’s newest users — without rewriting a line of business logic.
Primary sources
- expressjs.com — Using middleware (Express 5.x).
- expressjs.com — Security best practices.
- webmachinelearning.github.io/webmcp — W3C CG specification.
- github.com/webmachinelearning/webmcp — Declarative API explainer.
- developer.chrome.com/docs/ai/webmcp — origin trial.
- developer.chrome.com/docs/agents/security — agent threat model.
- mcptrail.com — WebMCP vs REST, with code.