Custom CDA queries
Three paths for custom Contentstack queries, from simplest to most integrated.
1. createCdaClient() — SDK access for custom tools
Best for custom tool execute handlers with full Delivery SDK access.
typescript
import { QueryOperation } from "@contentstack/delivery-sdk";
import { createCdaClient, createContentstackWebMcp } from "@timbenniks/contentstack-webmcp";
const cda = createCdaClient({ stack, contentTypes });
// Built-in helpers
await cda.search({ query: "headless" });
await cda.getByUrl("/blog/my-post");
await cda.getByUid("blogpost", "blt123");
await cda.listContentTypes();
// Custom tool with raw SDK query
await webmcp.registerCustomTools(
[
{
name: "products_by_sku",
description: "Find products by SKU prefix",
inputSchema: {
type: "object",
properties: { sku: { type: "string" } },
required: ["sku"],
},
annotations: { readOnlyHint: true, untrustedContentHint: true },
execute: async (input) => {
const result = await stack
.contentType("product")
.entry()
.query()
.where("sku", QueryOperation.STARTS_WITH, input.sku)
.find();
return { data: result.entries };
},
},
],
{ signal }
);Where to use
| Context | Use createCdaClient |
|---|---|
| Server custom route | Yes — pass server stack |
| Direct mode browser tool | Yes — pass client stack |
| Proxy mode browser tool | No — call your proxy/customRoutes instead |
2. customRoutes — custom proxy endpoints
Add bespoke GET routes alongside built-in proxy endpoints.
typescript
import { createContentstackWebMcpProxyHandler, ok } from "@timbenniks/contentstack-webmcp/server";
const handler = createContentstackWebMcpProxyHandler({
stack,
contentTypes,
basePath: "/api/contentstack",
customRoutes: {
"/products/by-sku": async (request, { stack, contentTypes, maxLimit }) => {
const sku = new URL(request.url).searchParams.get("sku");
const result = await stack
.contentType("product")
.entry()
.query()
.where("sku", QueryOperation.EQUALS, sku)
.find();
return ok({ data: result.entries ?? [] });
},
"/featured": async (_request, { stack }) => {
const result = await stack.contentType("page").entry().find();
const featured = (result.entries ?? []).filter((e) => e.featured);
return ok({ data: featured });
},
},
});Expose to agents via a custom tool:
typescript
{
name: "get_product_by_sku",
execute: async (input) => {
const res = await fetch(`/api/contentstack/products/by-sku?sku=${input.sku}`);
return res.json();
},
}Custom routes are matched before built-in routes. Keys are paths after basePath, e.g. '/products/by-sku'.
Context object
typescript
interface WebMcpProxyContext {
stack: ContentstackStack;
contentTypes: ContentTypeWebMcpConfig[];
locale?: string;
maxLimit: number;
}3. contentTypes[].serialize — transform output
Shape responses from built-in tools without changing query logic:
typescript
{
uid: "blogpost",
label: "Blog Post",
urlField: "url",
titleField: "title",
serialize: (entry) => ({
...entry,
reading_time: estimateReadingTime(entry),
}),
}Decision guide
| Need | Solution |
|---|---|
| Arbitrary SDK query in browser (demo) | registerCustomTools + stack |
| Arbitrary SDK query in production | customRoutes + custom tool calling fetch |
| Reuse built-in search/get with extra fields | serialize |
| Low-level CDA without WebMCP | createCdaClient() directly |