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.
npm install @timbenniks/contentstack-webmcp @contentstack/delivery-sdk reactHook
// 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:
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:
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
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:
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:
useContentstackWebMcp({
mode: "proxy",
proxyBasePath: "/api/contentstack",
contentTypes,
navigate: go,
});See Server runtimes for Express, Hono, Vite dev middleware, and serverless hosts.
Env vars
| Setup | Client-exposed prefix |
|---|---|
| Vite | VITE_ |
| Create React App | REACT_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.