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.