Skip to content

Experimental · Unofficial

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

Astro

Direct mode works with output: "static" — no adapter, no server. Astro's <script> in a .astro component is already bundled and client-side, so there is nothing to hydrate.

Client script

astro
---
// src/components/WebMcpInit.astro
---

<script>
  import contentstack from "@contentstack/delivery-sdk";
  import { createContentstackWebMcp, isWebMcpSupported } from "@timbenniks/contentstack-webmcp";
  import { contentTypes } from "../content-types";

  if (isWebMcpSupported()) {
    const stack = contentstack.stack({
      apiKey: import.meta.env.PUBLIC_CONTENTSTACK_API_KEY,
      deliveryToken: import.meta.env.PUBLIC_CONTENTSTACK_DELIVERY_TOKEN,
      environment: import.meta.env.PUBLIC_CONTENTSTACK_ENVIRONMENT,
    });

    const webmcp = createContentstackWebMcp({ mode: "direct", stack, contentTypes });
    const controller = new AbortController();

    await webmcp.register({
      signal: controller.signal,
      navigate: (path) => window.location.assign(path),
    });

    window.addEventListener("pagehide", () => controller.abort());
  }
</script>

Drop it into your layout:

astro
---
import WebMcpInit from "../components/WebMcpInit.astro";
---

<html>
  <body>
    <slot />
    <WebMcpInit />
  </body>
</html>

WARNING

No client:load here. Client directives only apply to framework components (React, Vue, Svelte). On a .astro component they are an error — and unnecessary, since its <script> already runs in the browser.

Env vars

Astro only exposes PUBLIC_-prefixed variables to client code. That prefix is the mode boundary:

bash
# .env — direct mode: the browser needs these
PUBLIC_CONTENTSTACK_API_KEY=...
PUBLIC_CONTENTSTACK_DELIVERY_TOKEN=...     # read-only
PUBLIC_CONTENTSTACK_ENVIRONMENT=production

# proxy mode: server-only, no prefix
CONTENTSTACK_API_KEY=...
CONTENTSTACK_DELIVERY_TOKEN=...
CONTENTSTACK_ENVIRONMENT=production

On Astro 5, astro:env gives you the same split with type safety — declare the delivery token as access: "public" for direct mode, "secret" for proxy.

View Transitions

With <ClientRouter />, a .astro <script> runs once on first load and not again after a transition — which is what you want, since tools stay registered. But pagehide does not fire on transitions either, so nothing is torn down until a real navigation. That is correct behaviour here.

If you moved init into an astro:page-load listener, guard against re-registering:

typescript
document.addEventListener("astro:page-load", () => {
  if (registered) return;
  registered = true;
  // …register once
});

Proxy mode

Needs output: "server" and an adapter (Node, Vercel, Netlify, Cloudflare).

typescript
// src/pages/api/contentstack/[...path].ts
import type { APIRoute } from "astro";
import contentstack from "@contentstack/delivery-sdk";
import { createContentstackWebMcpProxyHandler } from "@timbenniks/contentstack-webmcp/server";
import { contentTypes } from "../../../content-types";

const stack = contentstack.stack({
  apiKey: import.meta.env.CONTENTSTACK_API_KEY,
  deliveryToken: import.meta.env.CONTENTSTACK_DELIVERY_TOKEN,
  environment: import.meta.env.CONTENTSTACK_ENVIRONMENT,
});

const handler = createContentstackWebMcpProxyHandler({
  stack,
  contentTypes,
  basePath: "/api/contentstack",
  customRoutes: {
    "/featured": async (_request, { stack: cda }) => {
      const result = await cda
        .contentType("page")
        .entry()
        .query()
        .where("featured", "$eq", true)
        .find();
      return Response.json({ data: result.entries ?? [] });
    },
  },
});

export const GET: APIRoute = ({ request }) => handler(request);

Then in the client script, drop the stack and switch to { mode: "proxy", proxyBasePath: "/api/contentstack", contentTypes }.

Framework islands

If your interactive islands are React, you can use the /react hook inside one instead of a .astro script — mount it with client:load in that case, since it is a framework component.

Permissions-Policy

Static output has no server, so set it at your host — Netlify _headers, Vercel vercel.json, nginx. See Server runtimes.

Released under the MIT License.