sitecore-personalize-mcp
# MCP for Sitecore Personalize and CDP
A production-ready [Model Context Protocol](https://modelcontextprotocol.io) server for **Sitecore
Personalize / CDP**, built with the official `@modelcontextprotocol/sdk`, TypeScript, Zod, and Axios.
Every Sitecore Personalize API operation is exposed as its own MCP tool, so an MCP client (Claude
Desktop, Claude Code, or any other MCP host) can list/manage decisioning flows and experiences, read
and update CDP guest profiles, send behavioral events, and work with audiences and datasets.
## Architecture
```
src/
index.ts Process entry point: stdio transport wiring, signal handling
server.ts McpServer construction + tool registration
config/
env.ts Zod-validated environment configuration (fails fast at startup)
constants.ts API route table, server metadata, retry-status set
auth/
tokenManager.ts OAuth2 client_credentials flow, in-memory cache, refresh locking
services/
httpClient.ts Shared axios factory: auth injection, retry/backoff, error normalization
flowService.ts Flow list/get/publish/execute
experienceService.ts Experience list/get/publish
audienceService.ts Audience (segment) list/get/create
guestService.ts CDP guest get/upsert/delete/search
eventService.ts CDP event ingestion
datasetService.ts Dataset list/get
schemas/ One Zod schema module per domain (shared between tool input
validation and typed service calls)
tools/ One MCP tool per API operation, grouped by domain, registered
from tools/index.ts
utils/
logger.ts pino structured logger (stderr only — stdout is reserved for
MCP protocol frames)
errors.ts Typed error hierarchy (ValidationError, AuthenticationError,
SitecoreApiError, RetryExhaustedError, ConfigurationError)
retry.ts Exponential backoff w/ full jitter, used by httpClient
responseFormatter.ts Wraps service results into MCP CallToolResult (success/isError)
```
**Design principles:**
- **Clean separation of concerns.** Tools only translate MCP calls into service calls and format
results — they contain no HTTP or business logic. Services own API contracts. `httpClient` owns
cross-cutting HTTP concerns (auth, retry, logging, error shape) so every service gets them for free.
- **Every operation is its own tool.** No multiplexed "do anything" tool — each is independently
discoverable, documented, and schema-validated, which is what lets an MCP client (or the model
driving it) reason about what's safe to call.
- **Fail fast, fail loud.** Environment variables are validated once at startup with Zod; a misconfigured
deployment never gets as far as accepting a tool call.
- **stdout is sacred.** All logging goes to stderr via pino. Never `console.log` in this codebase —
it will corrupt the JSON-RPC stream on the stdio transport.
## Setup
```bash
npm install
cp .env.example .env
# edit .env with your tenant's client ID/secret and API URLs
npm run build
npm start
```
For local iteration with auto-reload: `npm run dev` (uses `tsx watch`).
### Required environment variables
| Variable | Description |
|---|---|
| `SITECORE_PERSONALIZE_CLIENT_ID` | OAuth2 client ID from Sitecore Cloud Portal |
| `SITECORE_PERSONALIZE_CLIENT_SECRET` | OAuth2 client secret |
| `SITECORE_PERSONALIZE_AUTH_URL` | Identity token endpoint |
| `SITECORE_PERSONALIZE_API_URL` | Personalize/CDP admin API base URL for your tenant/region |
| `SITECORE_PERSONALIZE_DECISIONING_API_URL` | *(optional)* Interactive decisioning/edge API base, if it differs from the admin API |
See `.env.example` for the full list, including HTTP timeout/retry tuning and log level.
> **Verify API routes before production use.** Sitecore Personalize's REST surface is versioned and
> tenant/region-hosted. The route table in `src/config/constants.ts` reflects the commonly documented
> v2/v3 shapes, but you should confirm exact paths against your tenant's current API reference before
> relying on this in production, and adjust that one file if anything differs.
### Connecting to Claude Desktop / Claude Code
Add to your MCP client config (e.g. `claude_desktop_config.json`):
```json
{
"mcpServers": {
"sitecore-personalize": {
"command": "node",
"args": ["/absolute/path/to/sitecore-personalize-mcp/dist/index.js"],
"env": {
"SITECORE_PERSONALIZE_CLIENT_ID": "...",
"SITECORE_PERSONALIZE_CLIENT_SECRET": "...",
"SITECORE_PERSONALIZE_AUTH_URL": "...",
"SITECORE_PERSONALIZE_API_URL": "...",
"SITECORE_CDP_CLIENT_KEY": "...",
"SITECORE_CDP_API_TOKEN": "...",
"SITECORE_CDP_API_URL": "..."
}
}
}
}
```
## Tools
All tool names are prefixed `sitecore_personalize_`.
| Tool | Type | Description |
|---|---|---|
| `flow_list` | read | List decisioning flows, filterable by status |
| `flow_get` | read | Get a single flow's definition |
| `flow_publish` | write | Publish a draft flow |
| `flow_execute` | write | Trigger real-time flow decisioning for a guest (`callFlow`) |
| `experience_list` | read | List experiences, filterable by type/status |
| `experience_get` | read | Get a single experience's definition |
| `experience_publish` | write | Publish a draft experience |
| `audience_list` | read | List audiences/segments |
| `audience_get` | read | Get a single audience's rules |
| `audience_create` | write | Create a new rule-based audience |
| `guest_get` | read | Get a CDP guest profile by reference |
| `guest_upsert` | write | Create or update a guest profile |
| `guest_delete` | write (destructive) | Permanently delete a guest profile |
| `guest_search` | read | Search guests by email or attribute |
| `event_send` | write | Ingest a behavioral event for a guest |
| `dataset_list` | read | List datasets |
| `dataset_get` | read | Get a single dataset's metadata |
Every write tool carries MCP `annotations` (`readOnlyHint`, `destructiveHint`, `idempotentHint`) so
clients can apply appropriate confirmation UX — `guest_delete` in particular is flagged destructive
and irreversible.
## Error handling & resilience
- **Validation** happens at the MCP layer via each tool's Zod `inputSchema` before any service code runs.
- **Auth failures** raise `AuthenticationError`; a `401` from the API triggers one transparent token
refresh + retry before failing.
- **Transient failures** (`429`, `5xx`, network errors) are retried with exponential backoff + full
jitter, up to `MAX_RETRIES` (default 3).
- **All failures** are normalized into typed errors and returned to the MCP client as
`{ isError: true, content: [...] }` — never as an uncaught exception that would kill the process or
return an opaque transport error.
## Extending
To add a new API operation:
1. Add its request/response shape to the relevant `src/schemas/*.schema.ts` (or a new file for a new domain).
2. Add the route to `src/config/constants.ts` and the call to the matching `src/services/*.ts`.
3. Register a tool for it in `src/tools/*.tools.ts`, following the existing pattern (`registerTool` →
service call → `toolSuccess`/`toolError`).
4. If it's a new domain, wire its `registerXTools(server)` into `src/tools/index.ts`.
## Scripts
| Command | Purpose |
|---|---|
| `npm run build` | Type-check and compile to `dist/` |
| `npm start` | Run the compiled server |
| `npm run dev` | Run with `tsx watch` for local development |
| `npm run typecheck` | Type-check without emitting |
| `npm run clean` | Remove `dist/` |
TDQS
Scored across 17 tools
The core resources (flow, audience, guest, dataset, event) are clearly separated, but flow and experience overlap heavily: experience_list/get are documented as exact equivalents of flow_list/get with a subtype pre-applied. This duplication is explained, but an agent could reasonably select either path for the same operation.
All tools follow a consistent sitecore_personalize_<resource>_<action> snake_case convention, such as flow_list, audience_get, guest_upsert, and event_send. The action verbs are predictable and there are no mixed casing or naming styles.
17 tools is at the upper edge but reasonable for a server spanning flows, experiences, audiences, guests, datasets, and events. The experience_list/get convenience duplicates add slight bloat, but most tools map to a distinct domain operation.
The guest lifecycle is well covered with get, upsert, delete, search, and data extension handling, and flows support listing, retrieving, status changes, and execution. However, audiences have create and read but no update/delete, flows cannot be created or updated beyond status, and datasets are read-only, leaving notable lifecycle gaps.