Vanilla JS / HTML
No framework, no adapter. This is the reference integration — everything else is the same three calls in a different lifecycle hook.
With a bundler (Vite, Webpack, Parcel, esbuild)
// src/webmcp.ts
import contentstack from "@contentstack/delivery-sdk";
import { createContentstackWebMcp, isWebMcpSupported } from "@timbenniks/contentstack-webmcp";
import { contentTypes } from "./content-types";
const stack = contentstack.stack({
apiKey: import.meta.env.VITE_CONTENTSTACK_API_KEY,
deliveryToken: import.meta.env.VITE_CONTENTSTACK_DELIVERY_TOKEN,
environment: import.meta.env.VITE_CONTENTSTACK_ENVIRONMENT,
});
/** Returns a teardown function. Call it on unload or SPA route teardown. */
export async function setupWebMcp() {
if (!isWebMcpSupported()) return () => {};
const webmcp = createContentstackWebMcp({ mode: "direct", stack, contentTypes });
const controller = new AbortController();
await webmcp.register({
signal: controller.signal,
navigate: (path) => location.assign(path),
});
return () => controller.abort();
}Call it once on app init:
// src/main.ts
import { setupWebMcp } from "./webmcp";
const teardown = await setupWebMcp();
addEventListener("pagehide", teardown);Webpack and Parcel use process.env.* instead of import.meta.env.* — the rest is identical.
No build step at all
A bare import "@timbenniks/contentstack-webmcp" will not resolve in a browser. Either add an import map, or point at a CDN build directly.
<!DOCTYPE html>
<html lang="en">
<head>
<script type="importmap">
{
"imports": {
"@timbenniks/contentstack-webmcp": "https://esm.sh/@timbenniks/contentstack-webmcp",
"@contentstack/delivery-sdk": "https://esm.sh/@contentstack/delivery-sdk"
}
}
</script>
</head>
<body>
<script type="module">
import contentstack from "@contentstack/delivery-sdk";
import { createContentstackWebMcp, isWebMcpSupported } from "@timbenniks/contentstack-webmcp";
if (isWebMcpSupported()) {
const stack = contentstack.stack({
apiKey: "your-api-key",
deliveryToken: "your-read-only-delivery-token",
environment: "production",
});
const webmcp = createContentstackWebMcp({
mode: "direct",
stack,
contentTypes: [
{
uid: "blogpost",
label: "Blog Post",
urlField: "url",
titleField: "title",
bodyField: "body",
searchableFields: ["title", "body"],
},
],
});
const controller = new AbortController();
await webmcp.register({
signal: controller.signal,
navigate: (path) => location.assign(path),
});
addEventListener("pagehide", () => controller.abort());
}
</script>
</body>
</html>WARNING
Permissions-Policy: tools=(self) must come from an HTTP response header on the document. <meta http-equiv="Permissions-Policy"> is silently ignored by browsers — http-equiv only honours a small fixed set of pragmas, and this is not one of them. Without the real header, tools register but no agent can reach them.
Static site generators
Eleventy, Hugo, Jekyll, and friends output plain HTML, so use either approach above and set the header at your host (Netlify _headers, Vercel vercel.json, nginx, Cloudflare). See Server runtimes for header snippets per host.
Proxy mode
If you would rather not ship the delivery token, drop the stack and point at your own routes:
const webmcp = createContentstackWebMcp({
mode: "proxy",
proxyBasePath: "/api/contentstack",
contentTypes,
});Then host the handler anywhere — see Server runtimes.