Skip to content

Experimental · Unofficial

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

React

React is the one framework with a shipped adapter: @timbenniks/contentstack-webmcp/react. It is a thin useEffect wrapper — feature detection, an AbortController tied to unmount, and the tool list in state. Nothing else.

bash
npm install @timbenniks/contentstack-webmcp @contentstack/delivery-sdk react

Hook

tsx
// src/WebMcp.tsx
import contentstack from "@contentstack/delivery-sdk";
import { useContentstackWebMcp } from "@timbenniks/contentstack-webmcp/react";
import { useNavigate } from "react-router-dom";
import { useCallback } from "react";
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();

  // Stable identity — the hook re-registers whenever `navigate` changes.
  const go = useCallback((path: string) => navigate(path), [navigate]);

  useContentstackWebMcp({ mode: "direct", stack, contentTypes, navigate: go });

  return null;
}

Mount it once, high in the tree:

tsx
export default function App() {
  return (
    <>
      <WebMcp />
      <Routes>{/* … */}</Routes>
    </>
  );
}

TIP

Wrap navigate in useCallback. The hook's effect depends on it, so an inline arrow re-registers every tool on every render.

Provider

If you would rather wrap children than mount a null component:

tsx
import { ContentstackWebMcpProvider } from "@timbenniks/contentstack-webmcp/react";

<ContentstackWebMcpProvider mode="direct" stack={stack} contentTypes={contentTypes} navigate={go}>
  <App />
</ContentstackWebMcpProvider>;

Add showDemoIndicator to render a small fixed badge with the registered tool count. Useful when demoing; leave it off in production.

Reading registration state

tsx
const { supported, registeredTools } = useContentstackWebMcp({
  mode: "direct",
  stack,
  contentTypes,
});

if (!supported) return <p>This browser has no WebMCP support yet.</p>;
return <p>{registeredTools.length} tools registered</p>;

Without the adapter

The adapter is optional. Plain useEffect is all it does:

tsx
useEffect(() => {
  if (!isWebMcpSupported()) return;

  const controller = new AbortController();
  const webmcp = createContentstackWebMcp({ mode: "direct", stack, contentTypes });
  void webmcp.register({ signal: controller.signal, navigate: go });

  return () => controller.abort();
}, [go]);

Proxy mode

Drop the stack and add a route on whatever serves your app:

tsx
useContentstackWebMcp({
  mode: "proxy",
  proxyBasePath: "/api/contentstack",
  contentTypes,
  navigate: go,
});

See Server runtimes for Express, Hono, Vite dev middleware, and serverless hosts.

Env vars

SetupClient-exposed prefix
ViteVITE_
Create React AppREACT_APP_
Webpack (manual)whatever you define via DefinePlugin

Only direct mode needs client-exposed credentials. In proxy mode the browser gets no token at all.

Released under the MIT License.