Skip to content

Experimental · Unofficial

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

Cloudflare Workers ​

Workers host the proxy handler. If you only want direct mode, you need no Worker at all — see Vanilla JS / HTML.

Worker ​

typescript
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 ​

toml
# 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_TOKEN

Custom routes ​

typescript
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:

typescript
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:

txt
/*
  Permissions-Policy: tools=(self)

If the Worker also serves HTML, set it there:

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

Released under the MIT License.