Skip to content

Experimental · Unofficial

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

Solid / SolidStart

onMount plus onCleanup maps directly onto register and abort.

Component

tsx
// src/components/WebMcp.tsx
import { onCleanup, onMount, createSignal } from "solid-js";
import { useNavigate } from "@solidjs/router";
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,
});

export function WebMcp() {
  const navigate = useNavigate();
  const [registered, setRegistered] = createSignal<string[]>([]);

  onMount(async () => {
    if (!isWebMcpSupported()) return;

    const controller = new AbortController();
    onCleanup(() => controller.abort());

    const webmcp = createContentstackWebMcp({ mode: "direct", stack, contentTypes });
    setRegistered(
      await webmcp.register({
        signal: controller.signal,
        navigate: (path) => navigate(path),
      })
    );
  });

  return null;
}

Mount it once in your root layout, above the router outlet, so it survives navigation.

TIP

Register onCleanup before awaiting. Registering it after the await puts it outside the owner's synchronous tracking scope, and it will never run.

SolidStart

SolidStart renders on the server, so keep the init in a component that only runs in the browser. isWebMcpSupported() already returns false when there is no document, so the guard above is enough — but placing it in onMount avoids constructing the stack during SSR at all.

tsx
// src/app.tsx
import { Router } from "@solidjs/router";
import { FileRoutes } from "@solidjs/start/router";
import { WebMcp } from "~/components/WebMcp";

export default function App() {
  return (
    <Router
      root={(props) => (
        <>
          <WebMcp />
          {props.children}
        </>
      )}
    >
      <FileRoutes />
    </Router>
  );
}

Proxy mode

SolidStart API routes speak Web Request/Response:

typescript
// src/routes/api/contentstack/[...path].ts
import type { APIEvent } from "@solidjs/start/server";
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 });

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

Then drop the stack from the component config and set mode: "proxy".

Plain Solid (no Start)

Use location.assign for navigate if you have no router, and see Vanilla JS / HTML for the bundler setup.

Released under the MIT License.