Nuxt
No adapter needed. A .client.ts plugin is the idiomatic home for this — it never runs during SSR, so there is no document guard to think about.
Client plugin
// plugins/webmcp.client.ts
import contentstack from "@contentstack/delivery-sdk";
import { createContentstackWebMcp, isWebMcpSupported } from "@timbenniks/contentstack-webmcp";
import { contentTypes } from "~/content-types";
export default defineNuxtPlugin(async () => {
if (!isWebMcpSupported()) return;
const config = useRuntimeConfig();
const router = useRouter();
const stack = contentstack.stack({
apiKey: config.public.contentstackApiKey,
deliveryToken: config.public.contentstackDeliveryToken,
environment: config.public.contentstackEnvironment,
});
const webmcp = createContentstackWebMcp({ mode: "direct", stack, contentTypes });
const controller = new AbortController();
await webmcp.register({
signal: controller.signal,
navigate: (path) => router.push(path),
});
window.addEventListener("pagehide", () => controller.abort());
});The .client suffix is what keeps this out of the server bundle. A plugin runs once per page load and survives client-side navigation, so tools register a single time.
Runtime config
Nuxt splits public and private for you, and that split is the mode boundary.
// nuxt.config.ts
export default defineNuxtConfig({
runtimeConfig: {
// Proxy mode — server only, never sent to the browser
contentstackApiKey: process.env.CONTENTSTACK_API_KEY,
contentstackDeliveryToken: process.env.CONTENTSTACK_DELIVERY_TOKEN,
contentstackEnvironment: process.env.CONTENTSTACK_ENVIRONMENT,
public: {
// Direct mode — the browser needs these
contentstackApiKey: process.env.NUXT_PUBLIC_CONTENTSTACK_API_KEY,
contentstackDeliveryToken: process.env.NUXT_PUBLIC_CONTENTSTACK_DELIVERY_TOKEN,
contentstackEnvironment: process.env.NUXT_PUBLIC_CONTENTSTACK_ENVIRONMENT,
},
},
});WARNING
Use public only for a read-only delivery token, and only in direct mode. Anything under public is serialised into the page payload.
Proxy mode
Nitro server routes need one adapter call — toWebRequest converts the H3 event to a Web Request.
// server/api/contentstack/[...path].ts
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,
basePath: "/api/contentstack",
});
export default defineEventHandler((event) => handler(toWebRequest(event)));Then in the plugin, drop the stack and switch to { mode: "proxy", proxyBasePath: "/api/contentstack", contentTypes } — no public runtime config needed at all.
Permissions-Policy
// nuxt.config.ts
export default defineNuxtConfig({
routeRules: {
"/**": { headers: { "Permissions-Policy": "tools=(self)" } },
},
});With nuxt generate (static output) there is no server, so set it at your host instead — see Server runtimes.