Skip to content

Experimental · Unofficial

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

Custom tools

Register app-specific tools alongside the built-in Contentstack tools.

Basic example

typescript
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:

typescript
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.

typescript
annotations: { readOnlyHint: true, untrustedContentHint: true }

CDA-backed custom tools

For Contentstack queries, see Custom CDA queries:

  • createCdaClient() — SDK helpers in direct mode or server
  • customRoutes — server endpoints called from custom tools in proxy mode

Tool definition shape

typescript
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:

typescript
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:

typescript
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.

Released under the MIT License.