Skip to content

Experimental · Unofficial

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

Server runtimes

Proxy mode needs somewhere to run createContentstackWebMcpProxyHandler. It takes a Web Request and returns a Web Response, so it drops into anything speaking that interface — and adapts to Node in about six lines.

Every example assumes:

typescript
import contentstack from "@contentstack/delivery-sdk";
import { createContentstackWebMcpProxyHandler } from "@timbenniks/contentstack-webmcp/server";
import { contentTypes } from "./content-types";

const stack = contentstack.stack({
  apiKey: process.env.CONTENTSTACK_API_KEY!,
  deliveryToken: process.env.CONTENTSTACK_DELIVERY_TOKEN!,
  environment: process.env.CONTENTSTACK_ENVIRONMENT!,
});

const handler = createContentstackWebMcpProxyHandler({
  stack,
  contentTypes,
  basePath: "/api/contentstack",
});

Hono

The closest fit — Hono is Web-standard end to end.

typescript
import { Hono } from "hono";

const app = new Hono();

app.get("/api/contentstack/*", (c) => handler(c.req.raw));

export default app;

Runs unchanged on Node, Bun, Deno, Cloudflare Workers, and Lambda.

Express

Express predates the Fetch API, so convert both directions.

typescript
import express from "express";

const app = express();

app.get("/api/contentstack/*", async (req, res) => {
  const url = `${req.protocol}://${req.get("host")}${req.originalUrl}`;
  const response = await handler(new Request(url, { headers: req.headers as HeadersInit }));

  res.status(response.status);
  response.headers.forEach((value, key) => res.setHeader(key, value));
  res.send(Buffer.from(await response.arrayBuffer()));
});

app.listen(3000);

Needs Node 18+ for global Request/Response.

Node (no framework)

typescript
import { createServer } from "node:http";

createServer(async (req, res) => {
  const response = await handler(new Request(`http://localhost${req.url}`));

  res.writeHead(response.status, Object.fromEntries(response.headers));
  res.end(Buffer.from(await response.arrayBuffer()));
}).listen(3000);

Deno

typescript
Deno.serve((request) => handler(request));

Read credentials with Deno.env.get("CONTENTSTACK_API_KEY") and run with --allow-env --allow-net.

Bun

typescript
Bun.serve({ port: 3000, fetch: handler });

Vercel functions

typescript
// api/contentstack/[...path].ts
export const GET = handler;

For the Next.js App Router, use the /next adapter instead.

Netlify functions

typescript
// netlify/functions/contentstack.mts
import type { Config } from "@netlify/functions";

export default (request: Request) => handler(request);

export const config: Config = { path: "/api/contentstack/*" };

Cloudflare Workers

Workers only expose env inside fetch, so build the stack there — or memoise it on first call. See Cloudflare Workers for the full setup including Wrangler config.

Vite dev server

Handy when the frontend is static but you still want proxy mode locally.

typescript
// vite.config.ts
export default defineConfig({
  plugins: [
    {
      name: "contentstack-webmcp",
      configureServer(server) {
        server.middlewares.use("/api/contentstack", async (req, res) => {
          const response = await handler(new Request(`http://localhost${req.originalUrl}`));
          res.statusCode = response.status;
          response.headers.forEach((v, k) => res.setHeader(k, v));
          res.end(Buffer.from(await response.arrayBuffer()));
        });
      },
    },
  ],
});

Permissions-Policy per host

The header goes on the document response, not the API response.

json
{
  "headers": [
    {
      "source": "/(.*)",
      "headers": [{ "key": "Permissions-Policy", "value": "tools=(self)" }]
    }
  ]
}
txt
/*
  Permissions-Policy: tools=(self)
nginx
add_header Permissions-Policy "tools=(self)" always;
apache
Header always set Permissions-Policy "tools=(self)"
typescript
app.use("*", async (c, next) => {
  await next();
  c.header("Permissions-Policy", "tools=(self)");
});

Custom routes

Every runtime above supports customRoutes for bespoke CDA queries alongside the built-in endpoints. See Custom CDA queries.

Released under the MIT License.