Cloudflare Workers
Workers host the proxy handler. If you only want direct mode, you need no Worker at all — see Vanilla JS / HTML.
Worker
import contentstack from "@contentstack/delivery-sdk";
import { createContentstackWebMcpProxyHandler } from "@timbenniks/contentstack-webmcp/server";
import { contentTypes } from "./content-types";
interface Env {
CONTENTSTACK_API_KEY: string;
CONTENTSTACK_DELIVERY_TOKEN: string;
CONTENTSTACK_ENVIRONMENT: string;
}
type Handler = (request: Request) => Promise<Response>;
let handler: Handler | undefined;
// `env` only exists inside fetch, so build on first request and reuse
// the handler for the lifetime of the isolate.
function getHandler(env: Env): Handler {
if (!handler) {
const stack = contentstack.stack({
apiKey: env.CONTENTSTACK_API_KEY,
deliveryToken: env.CONTENTSTACK_DELIVERY_TOKEN,
environment: env.CONTENTSTACK_ENVIRONMENT,
});
handler = createContentstackWebMcpProxyHandler({
stack,
contentTypes,
basePath: "/api/contentstack",
});
}
return handler;
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
return getHandler(env)(request);
},
};WARNING
Do not call contentstack.stack({ apiKey: env.… }) at module top level. In Workers, env is only bound inside fetch — at module scope it is undefined, and the Worker throws on first request.
Wrangler config
# wrangler.toml
name = "contentstack-webmcp-proxy"
main = "src/index.ts"
compatibility_date = "2026-01-01"
[vars]
CONTENTSTACK_ENVIRONMENT = "production"
# Secrets, not vars:
# wrangler secret put CONTENTSTACK_API_KEY
# wrangler secret put CONTENTSTACK_DELIVERY_TOKENCustom routes
handler = createContentstackWebMcpProxyHandler({
stack,
contentTypes,
basePath: "/api/contentstack",
customRoutes: {
"/featured": async (_request, { stack: cda }) => {
const result = await cda
.contentType("page")
.entry()
.query()
.where("featured", "$eq", true)
.find();
return Response.json({ data: result.entries ?? [] });
},
},
});Expose it to agents with a custom tool that fetches /api/contentstack/featured — see Custom CDA queries.
Pages + Worker
Host the static frontend on Cloudflare Pages and the proxy on a Worker route at /api/contentstack/*. The browser then needs no credentials:
createContentstackWebMcp({
mode: "proxy",
proxyBasePath: "/api/contentstack",
contentTypes,
});Permissions-Policy
The header belongs on the document response, not the proxy's JSON responses. With Pages, use a _headers file:
/*
Permissions-Policy: tools=(self)If the Worker also serves HTML, set it there:
const response = await getHandler(env)(request);
response.headers.set("Permissions-Policy", "tools=(self)");
return response;