Execution modes
Two ways for a tool handler to reach the CDA. Direct mode is the default: no backend, the Delivery SDK runs in the tab. Proxy mode puts a same-origin route in between when you need the delivery token off the client.
Direct mode (default)
Tool handlers call the Delivery SDK in the browser tab. No API routes, no server.
import contentstack from "@contentstack/delivery-sdk";
import { createContentstackWebMcp } from "@timbenniks/contentstack-webmcp";
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,
});
const webmcp = createContentstackWebMcp({ mode: "direct", stack, contentTypes });That's the whole integration. Works in static HTML, Astro islands, Vue, Svelte, Angular, and any bundler, with no server-side component at all.
What you are accepting
The delivery token ships in the JS bundle. It is read-only and scoped to one environment, and the content it reads is content the page already serves to every visitor — so for a public site the token itself is not much of a secret.
The consequence that does matter:
WARNING
A delivery token grants read access to every content type in that environment, not just the ones in your contentTypes config. Your allowlist is a tool-layer convenience, not a security boundary — anyone can lift the token from the bundle and query content types you never exposed.
So the prerequisite for direct mode is: everything published in that environment is content you are happy to have public. If the environment holds published-but-unlinked entries, staging pages, or internal records, use proxy mode.
Secondary considerations:
- CDA calls are metered. A token in a public bundle can be scraped and hammered.
- No shared cache — every tab fetches for itself.
- Your CSP must allow requests to the Contentstack CDA host.
Proxy mode
Tool handlers call same-origin routes; the routes use the Delivery SDK server-side.
// Server — credentials live here
import { createContentstackWebMcpProxyHandler } from "@timbenniks/contentstack-webmcp/server";
export default createContentstackWebMcpProxyHandler({ stack, contentTypes });
// Browser — no stack
createContentstackWebMcp({ mode: "proxy", proxyBasePath: "/api/contentstack", contentTypes });Reach for it when you need any of:
- The
contentTypesallowlist actually enforced — the token never leaves your server, so it is the only path to the CDA - Delivery token kept out of the bundle
- Shared caching, rate limiting, or error normalization in one place
- Custom server-side queries via
customRoutes - No CDA host in your CSP
Works with Node, Deno, Cloudflare Workers, and any Web Request/Response runtime. See Proxy endpoints for the route reference.
Comparison
| Direct | Proxy | |
|---|---|---|
| Server required | No | Yes |
| Token in browser | Yes (read-only, environment-scoped) | No |
contentTypes allowlist enforced | Tool layer only | Yes, server-side |
| Shared caching / rate limiting | No | Yes |
| Custom server routes | N/A | Yes (customRoutes) |
| CDA host in CSP | Required | Not required |
| Suitable for production | Yes, when the environment is fully public | Yes |
Choosing a mode
Public site, environment holds only public content → direct
Environment holds unlisted or internal entries → proxy
Need caching, quotas, or metering control → proxy
Need custom server-side CDA queries → proxy + customRoutes
No backend available at all → directBoth modes register identical tools and return identical response shapes, so switching later is a one-line config change plus a route.