Framework Guide · WebMCP · Strapi
WebMCP with Strapi: where headless APIs end and the agent surface begins
There is a category error waiting in every “WebMCP for headless CMS” article, and the honest version starts by refusing it: Strapi is an API, WebMCP is a browser API, and tools register in the tab that renders your content — never in the server that serves it. This article, written for platform engineers running Strapi behind a Next, Nuxt or Astro front end, maps what actually belongs where: the Content API as tool substrate, the SSR-guarded registration, the same-origin proxy as the privilege boundary, and the one-registry-two-transports pattern that keeps browser tools and MCP endpoints from drifting apart.
Fact-checked and updated on September 2, 2026 (Strapi 5 REST API documentation). Primary sources are cited at the end of the article.
Get the geography right and the rest is plumbing
The WebMCP standard, incubated in the W3C Web Machine Learning Community Group, exposes a browser interface — document.modelContext.registerTool() — through which a live page declares named, schema-typed actions an in-browser agent can invoke ([specification](https://webmachinelearning.github.io/webmcp); practical rollout per [Chrome for Developers](https://developer.chrome.com/docs/ai/webmcp): early preview in Chrome 146 behind the testing flag, public origin trial Chrome 149–156 from June 2026, token delivered via meta tag or header, canonical root migrating from navigator.modelContext to document.modelContext).
Strapi’s side of the picture is equally well-specified. Since v5, every content-type auto-generates REST endpoints under /api/{pluralApiId} with documentId-addressed documents, explicit populate for relations and media, flattened response data, and a permission posture the docs are blunt about: every content-type is private by default; public reads require explicit configuration or an API token ([Strapi 5 REST API reference](https://docs.strapi.io/cms/api/rest)).
Set side by side, the division of labor is obvious: Strapi decides what may be read or written, by whom, under which token. The browser layer decides what a visiting agent may do on a rendered page. Your job is to build the seam between them once, correctly — and every failure mode in this article is a story about hand-coding that seam twice.
Registering on the consuming app: the guard that is not optional
Whether your front end is Nuxt, Astro or plain static, registration must survive SSR — and Strapi shops overwhelmingly pick those frameworks precisely because their rendering models are heterogeneous. The canonical guard, in Nuxt with its .client component convention:
<!-- components/StrapiTools.client.vue -->
<script setup lang="ts">
import { onMounted, onUnmounted } from 'vue';
let controller: AbortController | undefined;
onMounted(async () => {
const ctx = document.modelContext ?? navigator.modelContext;
if (!ctx) return; // no trial, no cost, no trace
controller = new AbortController();
await ctx.registerTool({
name: 'browse-articles',
description: 'Browse published articles from the CMS. Optional category filter; returns titles, slugs, dates and excerpts (max 20).',
inputSchema: {
type: 'object',
properties: {
category: { type: 'string', description: 'Category slug, omit for all' },
page: { type: 'integer', minimum: 1, default: 1 },
},
additionalProperties: false,
},
annotations: { readOnlyHint: true },
async execute({ category, page = 1 }) {
const qs = new URLSearchParams({
'pagination[page]': String(page),
'pagination[pageSize]': '20',
'populate': 'author,cover', // explicit — never '*' in production
});
if (category) qs.set('filters[category][slug][$eq]', category);
const res = await fetch(`/api/articles?${qs}`); // same-origin proxy → Strapi
return { content: [{ type: 'text', text: JSON.stringify(await res.json()) }] };
},
}, { signal: controller.signal });
});
onUnmounted(() => controller?.abort()); // abort = unregister, per spec
</script>
<template><span hidden /></template>
The details are the standard contract from the specification and its Chrome documentation: feature detection on the dual root; the MCP content envelope on return; the AbortController signal as the sole deregistration mechanism, keyed to component lifecycle so tool availability tracks the page — not the session. The lifecycle-guard conventions differ per framework (Next’s 'use client', Astro’s plain <script>, Nuxt’s .client or import.meta.client, Svelte’s browser store) and are catalogued with worked examples in the MCP-B framework guide ([docs.mcp-b.ai](https://docs.mcp-b.ai/how-to/frameworks)).
The proxy is where your tokens live — and where the design is won
Strapi API tokens in client-registered tool code are Strapi API tokens in the browser: public. The read-only public content key is a legitimate exception; anything else belongs behind a same-origin proxy your front end already controls, forwarding the visitor’s session so Strapi answers as that user. This single architectural choice composes three security stories into one:
- Strapi’s default-private model stays the source of truth — the proxy forwards identity, not privilege.
- WebMCP’s “tools run with the visitor’s session” property ([spec](https://webmachinelearning.github.io/webmcp)) stops being a scary footnote and becomes your permission system, mirrored exactly.
- One place exists to flatten responses, enforce pagination, and rate-limit — all of which agents exercise at machine speed.
Design tools, not filter string generators
The seductive anti-pattern: one generic-query tool exposing Strapi’s rich filter syntax (filters[status][$eq]=published) to the agent. Powerful, and exactly wrong for LLM callers — models hallucinate field names, and worse, occasionally succeed, which is the failure mode no amount of retry logic survives. Chrome’s own guidance caps practical registries well below fifty tools per page, and the constraint forces the right design ([developer.chrome.com/docs/ai/webmcp](https://developer.chrome.com/docs/ai/webmcp)):
| Anti-pattern | Ship instead |
|---|---|
generic-query(collection, filter) |
browse-articles(category), get-article(slug), list-authors() |
| Unbounded results | Pagination enforced by the tool — a tool callable with pageSize=10000 will be |
| Raw Strapi JSON to the model | Flattened fields at the proxy: component trees and dynamic zones cost tokens and accuracy |
| Descriptions as afterthoughts | What it returns, limits, error behaviour — descriptions are routing inputs, not docs |
Strapi’s document metadata helps: publish dates, documentId, locale and slug are exactly the fields agent workflows need threaded through subsequent calls; include them deliberately, in stable shapes, and version the manifest (draft-spec churn is real — the root object has already moved once between Chromium builds).
One registry, two transports: the headless CMS’s endgame
Desktop MCP clients — Claude Desktop, Cursor, Windsurf — never render your Nuxt app; in-browser tools are invisible to them by construction. The bigger agent audience for most Strapi deployments (internal copilots, partner integrations, ingestion) therefore needs the server-side sibling: an MCP endpoint speaking tools/list and tools/call against the same services. The REST-versus-WebMCP trade-space — always-on and protocol-typed versus live-page and session-inheriting — is worked through with runnable code in our companion analysis ([MCP Trail comparison](https://mcptrail.com/blog/how-to-add-webmcp-to-a-website/)).
The pattern that survives contact with your roadmap: define each tool once — name, description, JSON Schema, handler reference — as a typed module; a build step emits the browser registration bundle and the MCP manifest from the same artifact. Same schemas, same tests, two transports; drift becomes structurally impossible. Strapi’s own extension points make the server half natural — lifecycle hooks and route middleware already centralize the populate and flatten logic ([docs.strapi.io — middlewares](https://docs.strapi.io/cms/middlewares)). If you run a decoupled architecture, the front-end guides in this series slot directly onto this side of the seam: Next.js, Astro, static HTML.
Verification, before anyone demos it
- DevTools on the rendered page, flag enabled:
await document.modelContext.getTools()— assert the exact registry;await document.modelContext.executeTool('browse-articles', '{"page":1}')— assert the envelope and the proxy’s flattening. - Privilege test, the one teams skip: run every read-tool’s call as an anonymous visitor, as a logged-in member, and as admin — the responses must differ exactly as the underlying endpoints do.
- Through the bridge with one real MCP client (Claude Desktop against your manifest) — the transport your biggest audience actually uses today.
- The Model Context Tool Inspector extension for schema inspection and Gemini-simulated routing.
FAQ
Can Strapi serve WebMCP directly — no front end?
No. Registration is a browser operation; Strapi serves data. What Strapi can do — and should — is serve the tool definitions as data (content-types or a config file) that your front end and MCP bridge both consume. “Strapi + WebMCP” always means “Strapi-fed front end + WebMCP.”
What about writes — orders, contact forms, comments?
Tools may call them; the rules are the rules: validation server-side, CSRF and capability checks exactly as for human form posts, human confirmation for consequential actions — the declarative API’s default toolautosubmit-off posture exists for precisely this class of action ([explainer](https://github.com/webmachinelearning/webmcp/blob/main/declarative-api-explainer.md)).
Does multi-locale or draft/publish affect the surface?
Model both explicitly in tool parameters and defaults — an agent given an ambiguous locale or a draft-inclusive read will surface the wrong content confidently. Constrain the schema; let the proxy apply the same locale and publicationState defaults your human UI applies.
Conclusion
Strapi spent a decade making content API-first — typed schemas, generated routes, default-private permissions, explicit population. WebMCP is not a new integration burden on that architecture; it is a new client of it, arriving through the browser your front end already owns. Keep the registration guarded, the privileges mirrored, the descriptions authored like the interface copy they are, and your CMS stops being a content warehouse with a login page and becomes the backend of choice for the next category of visitor: the one that never loads your CSS.