Security
Read-only by default
All CDA-backed tools are read-only and use readOnlyHint: true. No CMA tools, publish/unpublish actions, or asset uploads.
Untrusted content
The three CDA entry tools set untrustedContentHint: true. Entries carry author-written content this package does not validate, so agents are told to treat tool output as data rather than instructions. Set the same hint on any custom tool returning content you did not author.
Choosing a mode
Direct mode is the default and is fine in production when everything published in the environment is content you are happy to have public. The delivery token is read-only and environment-scoped, but it grants read access to every content type in that environment — your contentTypes allowlist is not a security boundary in direct mode.
Use proxy mode when the environment holds published-but-unlisted entries, staging pages, or internal records, or when you need the allowlist enforced server-side.
Input validation
Built-in protections:
- Search
limitcapped atsecurity.maxLimit(default 50), and the tool's input schema advertises the same cap - Search text capped at 200 characters and 8 tokens
- Search tokens are regex-escaped before reaching the CDA, so a visitor's query cannot alter the query structure or supply a pathological pattern
- URL paths normalized; open redirects blocked in
navigate_to - Tools are same-origin only unless you widen them with
exposedTo
Optional rate limiting in proxy middleware per IP or session.
Cross-origin exposure
Tools are visible only to same-origin documents by default. To widen that:
await webmcp.register({
signal: controller.signal,
exposedTo: ["https://agent.example.com"],
});Permissions-Policy
Customers should set:
Permissions-Policy: tools=(self)Vercel (vercel.json)
{
"headers": [
{
"source": "/(.*)",
"headers": [{ "key": "Permissions-Policy", "value": "tools=(self)" }]
}
]
}Next.js (next.config.js)
module.exports = {
async headers() {
return [
{
source: "/:path*",
headers: [{ key: "Permissions-Policy", value: "tools=(self)" }],
},
];
},
};nginx
add_header Permissions-Policy "tools=(self)" always;Cloudflare Workers
const response = await handler(request);
response.headers.set("Permissions-Policy", "tools=(self)");
return response;Environment kill switch
Disable WebMCP entirely:
CONTENTSTACK_WEBMCP_ENABLED=falseFramework-specific aliases: NEXT_PUBLIC_CONTENTSTACK_WEBMCP_ENABLED, VITE_CONTENTSTACK_WEBMCP_ENABLED.
Custom route security
When adding customRoutes:
- Validate and sanitize all query parameters
- Never expose management tokens
- Return errors without leaking stack internals
- Keep routes read-only unless explicitly intended otherwise
What not to do
- Expose CMA or management tokens in browser tools — ever
- Use direct mode against an environment holding content you would not publish
- Treat the
contentTypesallowlist as an access-control boundary in direct mode - Allow
navigate_toto external URLs (built-in guard rejects non-internal paths) - Skip
AbortSignalcleanup (can leak registered tools on SPA navigation)