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:
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.
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.
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)
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
Deno.serve((request) => handler(request));Read credentials with Deno.env.get("CONTENTSTACK_API_KEY") and run with --allow-env --allow-net.
Bun
Bun.serve({ port: 3000, fetch: handler });Vercel functions
// api/contentstack/[...path].ts
export const GET = handler;For the Next.js App Router, use the /next adapter instead.
Netlify functions
// 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.
// 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.
{
"headers": [
{
"source": "/(.*)",
"headers": [{ "key": "Permissions-Policy", "value": "tools=(self)" }]
}
]
}/*
Permissions-Policy: tools=(self)add_header Permissions-Policy "tools=(self)" always;Header always set Permissions-Policy "tools=(self)"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.