Skip to content

Experimental · Unofficial

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

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)

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

typescript
// 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.

html
<!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:

typescript
const webmcp = createContentstackWebMcp({
  mode: "proxy",
  proxyBasePath: "/api/contentstack",
  contentTypes,
});

Then host the handler anywhere — see Server runtimes.

Released under the MIT License.