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.