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.

Key takeaway: WebMCP in Express is a rendering concern, like Open Graph tags — except the tag has a function body. Your routers stay the contract; the agent surface is one more thing your templates negotiate with the browser.

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.

Authoring rule: the description string is the only documentation an LLM will ever read. State what the tool does, what it returns, and its bounds — “up to 20 products,” “prices in EUR, taxes excluded.” Inputs that must not be free text get <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

  1. A tool’s execute runs 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 inside execute exactly as you validate req.body: tool parameters are user input wearing nicer clothes ([Express security best practices](https://expressjs.com/en/advanced/best-practice-security.html)).
  2. Never interpolate tool arguments into queries, shell calls or view paths. The classic Express footguns — unvalidated req reaching res.render or a raw SQL string — apply with a new, faster attacker.
  3. 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.
  4. 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

  1. expressjs.com — Using middleware (Express 5.x).
  2. expressjs.com — Security best practices.
  3. webmachinelearning.github.io/webmcp — W3C CG specification.
  4. github.com/webmachinelearning/webmcp — Declarative API explainer.
  5. developer.chrome.com/docs/ai/webmcp — origin trial.
  6. developer.chrome.com/docs/agents/security — agent threat model.
  7. mcptrail.com — WebMCP vs REST, with code.

Leave a Reply

Your email address will not be published. Required fields are marked *