Skip to content

Experimental · Unofficial

Built by Tim Benniks. This is not an official Contentstack product and is not supported by Contentstack.

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 limit capped at security.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:

typescript
await webmcp.register({
  signal: controller.signal,
  exposedTo: ["https://agent.example.com"],
});

Permissions-Policy

Customers should set:

Permissions-Policy: tools=(self)

Vercel (vercel.json)

json
{
  "headers": [
    {
      "source": "/(.*)",
      "headers": [{ "key": "Permissions-Policy", "value": "tools=(self)" }]
    }
  ]
}

Next.js (next.config.js)

javascript
module.exports = {
  async headers() {
    return [
      {
        source: "/:path*",
        headers: [{ key: "Permissions-Policy", value: "tools=(self)" }],
      },
    ];
  },
};

nginx

nginx
add_header Permissions-Policy "tools=(self)" always;

Cloudflare Workers

typescript
const response = await handler(request);
response.headers.set("Permissions-Policy", "tools=(self)");
return response;

Environment kill switch

Disable WebMCP entirely:

bash
CONTENTSTACK_WEBMCP_ENABLED=false

Framework-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 contentTypes allowlist as an access-control boundary in direct mode
  • Allow navigate_to to external URLs (built-in guard rejects non-internal paths)
  • Skip AbortSignal cleanup (can leak registered tools on SPA navigation)

Released under the MIT License.