Custom tools
Register app-specific tools alongside the built-in Contentstack tools.
Basic example
await webmcp.registerCustomTools(
[
{
name: "open_support_chat",
description: "Open the support chat widget",
inputSchema: { type: "object", properties: {} },
execute: async () => ({ opened: true }),
},
],
{ signal: controller.signal }
);Results
execute resolves to any serializable value and the agent receives it directly — no content envelope. Return your data as-is:
import { toolError, safeExecute } from "@timbenniks/contentstack-webmcp";
// Success — just return the value
return { data: entries };
// Error
return toolError("Entry not found");
// Or let safeExecute turn a thrown error into that shape
return safeExecute(async () => {
const data = await fetchSomething();
return data;
});Annotations
Set untrustedContentHint: true on any tool that returns content you have not validated — CMS entries, user submissions, third-party APIs. It tells the agent to treat the output as data rather than instructions. The built-in CDA entry tools set it already.
annotations: { readOnlyHint: true, untrustedContentHint: true }CDA-backed custom tools
For Contentstack queries, see Custom CDA queries:
createCdaClient()— SDK helpers in direct mode or servercustomRoutes— server endpoints called from custom tools in proxy mode
Tool definition shape
interface WebMcpToolDefinition {
name: string;
title?: string;
description: string;
inputSchema?: Record<string, unknown>; // JSON Schema
execute: (input: Record<string, unknown>) => Promise<unknown>;
annotations?: {
readOnlyHint?: boolean;
untrustedContentHint?: boolean;
};
}Toggle built-in tools
Disable built-in tools you don't need:
createContentstackWebMcp({
mode: "proxy",
contentTypes,
tools: {
search: true,
getByUrl: true,
getByUid: false,
listContentTypes: true,
getCurrentPage: true,
getSiteInfo: true,
navigateTo: true,
},
});Then add your custom tools after registration.
Cleanup
Always pass an AbortSignal and abort on teardown:
const controller = new AbortController();
await webmcp.register({ signal: controller.signal, navigate });
await webmcp.registerCustomTools(customTools, { signal: controller.signal });
window.addEventListener("pagehide", () => controller.abort());In React, the hook handles this automatically via useEffect cleanup.