{
  "markdown": "<div align=\"center\">\n\n![AEP MCP Server](https://raw.githubusercontent.com/Focus-GTS/aep-mcp-server/main/hero.svg)\n\n[![npm](https://img.shields.io/npm/v/%40focusgts%2Faep-mcp-server?color=CB3837&label=npm&logo=npm)](https://www.npmjs.com/package/@focusgts/aep-mcp-server)\n[![installs](https://img.shields.io/npm/dm/%40focusgts%2Faep-mcp-server?color=CB3837&label=installs%2Fmo)](https://www.npmjs.com/package/@focusgts/aep-mcp-server)\n[![MCP Registry](https://img.shields.io/badge/MCP_Registry-listed-6E56CF)](https://registry.modelcontextprotocol.io)\n[![tests](https://img.shields.io/badge/tests-509%20passing-brightgreen)](#-development)\n[![license](https://img.shields.io/badge/license-Apache--2.0-blue)](LICENSE)\n\n### Adobe's MCP lets your agent *read* Experience Platform. This one lets it **work**.\n\n**61 tools across 14 categories. Full read AND write. Self-hosted, Apache-2.0, no invitation required.**\nExperience Platform, Journey Optimizer and Customer Journey Analytics — from one OAuth credential.\n\n**Ingest a batch → compose a schema → activate an audience → honour an erasure.**\nEvery mutation gated by a fail-closed write guard that asks Adobe what kind of sandbox it's in.\n\n![Ask your agent](https://readme-typing-svg.demolab.com/?font=JetBrains+Mono&size=20&duration=2600&pause=800&color=6E56CF&center=true&vCenter=true&width=760&height=42&lines=%22Ingest+this+file+into+a+new+dataset%22;%22Build+an+audience+and+activate+it%22;%22Honour+this+GDPR+erasure+request%22;%22What's+my+data+lifecycle+quota%3F%22)\n\n</div>\n\n---\n\n## ⚡ Do it in three lines\n\n```bash\nclaude mcp add aep \\\n  -e AEP_CLIENT_ID=... -e AEP_CLIENT_SECRET=... \\\n  -e AEP_ORG_ID=...@AdobeOrg -e AEP_SANDBOX_NAME=your-dev-sandbox \\\n  -- npx -y @focusgts/aep-mcp-server\n```\n\nThen just ask your agent:\n\n> *\"Create a schema with the Demographic Details field group, then a dataset on it.\"*\n> *\"Ingest this NDJSON file and tell me when the batch lands.\"*\n> *\"Build an audience of customers who bought twice this quarter and activate it.\"*\n> *\"Delete every record for this email address — dry run first.\"*\n\nWrites are off until you ask for them, and `safe` mode only unlocks sandboxes **Adobe** classifies as development.\n\n### The loop that makes it different\n\n```mermaid\nflowchart LR\n  A[\"📐 Compose<br/>schema from field groups\"] --> B[\"🗂️ Create<br/>dataset\"]\n  B --> C[\"📥 Ingest<br/>batch · upload · complete\"]\n  C --> D[\"🎯 Activate<br/>segment → destination\"]\n  D --> E[\"🧹 Govern<br/>erasure · expiration · quota\"]\n  E -. \"re-audit the tenant\" .-> A\n```\n\nAdobe's first-party gateway can *tell you* what's in your Experience Platform tenant. It cannot create a dataset, land a batch, activate an audience, or submit an erasure. This does — and does it behind a guard that fails closed.\n\n---\n\n## 🧠 How it works\n\n```mermaid\nflowchart LR\n  A[\"AI agent<br/>(Claude · Cursor · Copilot)\"] -- MCP / stdio --> B[\"aep-mcp-server<br/>61 tools\"]\n  B --> W{{\"write guard<br/>fail-closed\"}}\n  W --> C[\"Schema Registry · Catalog<br/>Ingestion · Lifecycle · Privacy\"]\n  C --> F[\"Your AEP sandbox<br/>platform.adobe.io\"]\n  B --> J[\"Journey Optimizer<br/>read-only\"]\n  J --> K[\"ajo campaigns\"]\n  B --> Q[\"Customer Journey Analytics<br/>read-only, no sandbox\"]\n  Q --> L[\"cja.adobe.io\"]\n```\n\nThe agent calls tools; the server talks to live Adobe APIs over OAuth Server-to-Server. **The write guard sits in the HTTP client, not in each tool**, so all 61 inherit it and none can forget it. Blocked calls never reach Adobe.\n\n---\n\n## 🛡️ Safe by default\n\nThree postures. **Reads are never restricted in any mode.**\n\n| `AEP_MODE` | Writes permitted | Use it when |\n|---|---|---|\n| `read-only` | Never, in any sandbox | Handing the server to someone to explore an environment you don't want touched |\n| **`safe`** *(default)* | Only where Adobe classifies the sandbox `development` | Evaluating, or letting an agent work without risking production |\n| `production` | Anywhere, including production | You run your own change control and don't want the server second-guessing you |\n\n> **How `safe` decides — and why it's not the sandbox name.**\n> A production sandbox can be called anything, and a sandbox called `prod` might not be production. Only Adobe's `type` field from the Sandbox Management API decides.\n>\n> **It fails closed.** If the type can't be determined — the credential can't read sandbox metadata, the API errors, startup hasn't finished — writes are blocked. A credential must not earn write access by being *less* capable. An unrecognised `AEP_MODE` falls back to `safe`, so a typo can never grant production writes.\n>\n> **A sandbox literally named `prod` is refused unconditionally**, before mode resolution — so `AEP_MODE=production` does not lift it. Override with `AEP_I_UNDERSTAND_THIS_WRITES_TO_PROD=true` only if that really is your sandbox's name. The inference is deliberately asymmetric: trusting a name to *allow* a write is unsafe, trusting one to *deny* a write is safe, because the worst case is a refusal you can override on purpose.\n>\n> **Mutations are off entirely** unless `AEP_ALLOW_MUTATIONS=true`. That is separate from `AEP_MODE` on purpose: choosing a write mode should not also mean \"yes, you may change my data\".\n\nStartup always states the active posture:\n\n```\nSAFE MODE — sandbox is a development sandbox, so writes are ENABLED.\nSAFE MODE — sandbox is PRODUCTION, so writes are BLOCKED. Reads work normally.\nSAFE MODE — sandbox type could not be confirmed, so writes are BLOCKED (fail-closed).\nREAD-ONLY MODE — no write, update, or delete will be performed in any sandbox.\nPRODUCTION MODE — writes permitted against ANY sandbox, including production.\n```\n\n### Per-tool confirmation gates\n\nWrites are not *uniformly* gated — uniform gating makes an agent useless. Gates sit where an action is irreversible and wide-reaching, and every one is checked **before any network call**:\n\n| Tool | Gate |\n|---|---|\n| `aep_create_record_delete` | `dryRun` defaults **true**. Real submission needs `confirm: \"DELETE RECORDS <datasetId> <identityDigest>\"` — bound to the dataset **and** a SHA-256 digest of the exact identity set, so a confirmation can't be reused for a different deletion. `ALL` and multi-dataset targets are **refused**. |\n| `aep_delete_segment` | `confirm: \"DELETE SEGMENT <segmentId>\"` — segments were create-only until 0.9.1, so every one an agent made was permanent |\n| `aep_delete_dataset` | `confirm: \"DELETE DATASET <id>\"`, escalating to `\"DELETE PROFILE-ENABLED DATASET <id>\"` when the dataset feeds Profile |\n| `aep_complete_batch` | `confirm: \"COMPLETE BATCH <batchId>\"` — the point of no return for ingestion |\n| `aep_revert_batch` | `confirm: \"REVERT BATCH <batchId>\"` |\n| `aep_create_dataset_expiration` | `confirm: \"CREATE DATASET EXPIRATION <datasetId>\"` — unless `dryRun: true` |\n| `aep_update_dataset_expiration` | `confirm: \"UPDATE DATASET EXPIRATION <ttlId>\"` |\n| `aep_cancel_dataset_expiration` | `confirm: \"CANCEL DATASET EXPIRATION <ttlId>\"` |\n| `aep_delete_profile` | `confirm: \"I understand this is irreversible\"` *(deprecated — prefer Data Hygiene)* |\n\n> **Confirmations name their target.** A phrase carrying the dataset id — and for record delete, a hash of the identities too — cannot be copied from one call to another. A generic \"I understand this is irreversible\" approves *any* deletion once you've typed it once.\n>\n> **Identity values never leave the process.** `aep_create_record_delete` returns a count, the namespace names, and a digest — never the email addresses or device IDs you passed it. A record-delete request is by nature a list of real people; a tool that echoes them copies them into every transcript and log sink it touches.\n>\n> Batch creation and file upload are ungated on purpose: those writes are additive and recoverable. An unwanted batch can be left uncompleted, and data that did land can be removed with the Data Hygiene tools.\n\n### Tool annotations\n\nEvery tool ships MCP annotations — `readOnlyHint`, `destructiveHint`, `idempotentHint`, `openWorldHint` — derived from the same metadata that builds its description, so the two cannot drift.\n\n| | Count |\n|---|---|\n| `readOnlyHint: true` | 39 |\n| `destructiveHint: true` | 8 |\n| Un-annotated | **0** |\n\nThese are hints *for the client*, not enforcement — the guards above enforce. Their value is that a client like Claude Desktop uses `destructiveHint` to decide when to interrupt and ask a human. Without them `aep_delete_profile` looks identical to `aep_list_schemas`. A test asserts the destructive list exactly, so a ninth is a deliberate act rather than an oversight.\n\n---\n\n## 🛠️ The 61 tools\n\nAll prefixed `aep_`, `verb_noun` naming. 🔒 changes state · 🔥 destructive.\n\n### Data modelling & ingestion\n\n<table>\n<tr><td valign=\"top\" width=\"33%\">\n\n**Schemas** (4)\n- `list_schemas`\n- `get_schema`\n- `create_schema` 🔒\n- `update_schema` 🔒\n\n**Datasets** (4)\n- `list_datasets`\n- `get_dataset`\n- `create_dataset` 🔒\n- `delete_dataset` 🔥\n\n</td><td valign=\"top\" width=\"33%\">\n\n**Ingestion** (7)\n- `create_batch` 🔒\n- `upload_batch_file` 🔒\n- `complete_batch` 🔥\n- `get_batch_status`\n- `list_batches`\n- `abort_batch` 🔥\n- `revert_batch` 🔥\n\n</td><td valign=\"top\" width=\"33%\">\n\n**Sources** (2)\n- `list_sources`\n- `list_dataflows`\n\n**Query Service** (3)\n- `run_query` 🔒\n- `get_query_status`\n- `list_queries`\n\n</td></tr>\n</table>\n\n### Profiles, audiences & activation\n\n<table>\n<tr><td valign=\"top\" width=\"33%\">\n\n**Identities** (2)\n- `list_identity_namespaces`\n- `get_identity_graph`\n\n</td><td valign=\"top\" width=\"33%\">\n\n**Profiles** (4)\n- `get_profile`\n- `get_profile_by_identity`\n- `preview_profile`\n- `delete_profile` 🔥\n\n</td><td valign=\"top\" width=\"33%\">\n\n**Segments** (5)\n- `list_segments`\n- `get_segment`\n- `create_segment` 🔒\n- `estimate_segment_size`\n- `delete_segment` 🔥\n\n**Destinations** (3)\n- `list_destinations`\n- `create_destination_connection` 🔒\n- `activate_segment` 🔒\n\n</td></tr>\n</table>\n\n### Governance — privacy, lifecycle & event routing\n\n<table>\n<tr><td valign=\"top\" width=\"33%\">\n\n**Privacy Service** (6)\n- `create_privacy_job` 🔒\n- `get_privacy_job`\n- `list_privacy_jobs`\n- `cancel_privacy_job` 🔒\n- `get_privacy_job_results`\n- `list_privacy_namespaces`\n\n</td><td valign=\"top\" width=\"33%\">\n\n**Data Hygiene** (9)\n- `create_record_delete` 🔥\n- `get_work_order_status`\n- `list_work_orders`\n- `get_data_lifecycle_quota`\n- `create_dataset_expiration` 🔥\n- `get_dataset_expiration`\n- `list_dataset_expirations`\n- `update_dataset_expiration` 🔥\n- `cancel_dataset_expiration` 🔥\n\n</td></tr>\n</table>\n\n> **Workflows these unlock**\n>\n> **Ingest end to end** — `create_schema` → `create_dataset` → `create_batch` → `upload_batch_file` → `complete_batch` → `get_batch_status`\n>\n> **Build and activate an audience** — `create_segment` → `estimate_segment_size` → `list_destinations` → `create_destination_connection` → `activate_segment`\n>\n> **Honour an erasure request** — `get_profile_by_identity` → `create_record_delete` → `get_work_order_status`\n>\n> **Retire data on a schedule** — `create_dataset_expiration` → `list_dataset_expirations` → `update_dataset_expiration` → `cancel_dataset_expiration`\n\n**What's actually been run against a live tenant** is recorded per tool in [`docs/VALIDATION-MATRIX.md`](./docs/VALIDATION-MATRIX.md) — including the surfaces that are documented-and-mocked but deliberately never executed, and why.\n\n\n### Adobe Journey Optimizer (2)\n\nAJO is a **separate Adobe product, licensed separately** — hence the `ajo_` prefix, so an entitlement failure reads as one.\n\n<table>\n<tr><td valign=\"top\" width=\"50%\">\n\n**Campaigns**\n- `ajo_list_campaigns`\n- `ajo_get_campaign`\n\n</td><td valign=\"top\" width=\"50%\">\n\nCampaigns is the **only** AJO surface reachable on our tenant. Journeys, messages, channel surfaces, content templates, fragments, offers and decisions all return an HTML 404 — the gateway has no such route — so they are deliberately not implemented.\n\n</td></tr>\n</table>\n\n> Writes are absent on purpose. The routes exist, but shipping an unvalidated write path into a product that sends messages to real people is not a trade worth making.\n\n---\n\n## 📊 AEC-Bench — does your agent actually work?\n\nEvery MCP server in this space is described by its tool count. That measures surface area, not competence: **fifty tools that 404 score higher than ten that work.**\n\n`bench/` is an agentic benchmark that measures the other thing — given a real task and a live tenant, does the agent finish it, and can you prove it?\n\n```bash\nnpm run bench          # tier 1, read-only, safe on any tenant\nnpm run bench:write    # tier 2, creates and removes what it creates\n```\n\n| | |\n|---|---|\n| **Assertions run against Adobe** | A \"create a segment\" task is scored by a GET that finds it — never by the create call's own success flag. A write reporting on itself is not evidence. |\n| **Cleanup is scored** | Completing the goal while leaving an orphan is not a pass. A benchmark that dirties the tenant can only run once honestly. |\n| **Tier 1 is production-safe** | GET only. A benchmark nobody dares run measures nothing. |\n\nCurrent: **tier 1 5/5, tier 2 2/2, zero residue.** Tier 3 (irreversible) is defined and deliberately empty — its tasks are non-cancellable and can take 30 days, and a benchmark is not a good reason to run one.\n\nWe expect to score badly on tasks we haven't built for. That's the intended use.\n\n\n## 📈 Customer Journey Analytics (10)\n\n**One OAuth credential now serves three Adobe services.** Add the Customer Journey Analytics API to the same Developer Console project that owns `AEP_CLIENT_ID`, and the `cja_*` tools light up — no second secret, no separate auth.\n\n<table>\n<tr><td valign=\"top\" width=\"50%\">\n\n**Discover**\n- `cja_list_companies`\n- `cja_list_connections`\n- `cja_get_connection`\n- `cja_list_data_views`\n- `cja_get_data_view`\n\n</td><td valign=\"top\" width=\"50%\">\n\n**Report**\n- `cja_list_dimensions`\n- `cja_list_metrics`\n- `cja_list_segments`\n- `cja_list_calculated_metrics`\n- `cja_run_report`\n\n</td></tr>\n</table>\n\n### The AEP sandbox and the CJA company are not the same thing\n\n| | AEP | CJA |\n|---|---|---|\n| Host | `platform.adobe.io` | `cja.adobe.io` |\n| Scope unit | **sandbox** (`AEP_SANDBOX_NAME`) | **global company id** — or the IMS org |\n| Header | `x-sandbox-name` | `x-proxy-global-company-id` *(optional — see below)* |\n| Maps to the other? | **No.** A CJA connection or data view has no one-to-one relationship with any AEP sandbox. |\n\nThe CJA client **never sends `x-sandbox-name`.** CJA has no sandbox concept, and attaching one would be meaningless at best and misleading in a trace.\n\n### Company discovery, and why it may not work\n\nAdobe's documented discovery endpoint is `GET https://analytics.adobe.io/discovery/me`. **That host belongs to Adobe Analytics — a different product from CJA.** A credential entitled to CJA but not Analytics gets `403003 Api Key is invalid` there: the key is fine, it simply has no Analytics entitlement. CJA exposes no discovery of its own.\n\nIn practice this blocks nothing: **CJA answers every resource with `x-gw-ims-org-id` alone**, so the company id is optional.\n\n```bash\n# Optional. Omit it and CJA scopes by IMS org, which is what works by default.\nCJA_GLOBAL_COMPANY_ID=your-global-company-id\n```\n\n`cja_list_companies` reports which context is in use and whether discovery is reachable. If discovery ever returns **several** companies and no override is set, it refuses to pick one — silently choosing the first would point every subsequent report at the wrong company.\n\n### Running the probes and the live tests\n\n```bash\nnode scripts/probe-cja.mjs --env .env          # read-only, sanitized output\nCJA_LIVE_TESTS=1 npm test -- tests/integration/cja-live.test.ts\n```\n\nThe live tests are **skipped unless `CJA_LIVE_TESTS=1`**, so `npm test` stays hermetic. They are read-only and never print credentials or full Adobe responses.\n\n### Current limitations, honestly\n\n- **No mutation tools.** This slice is read-only by design.\n- **Discovery is unavailable** on a CJA-only credential, as above. Not a defect in the tools.\n- **`cja_get_connection` is unvalidated** — the validation tenant has zero connections, and an id is never fabricated to manufacture a pass.\n- **Paging on dimensions and metrics is inert.** CJA wraps them in a `content` envelope that looks pageable, but `limit` and `page` are ignored — verified live, all 38 dimensions returned regardless. `search`, `limit` and `offset` are applied client-side, and the output says so.\n\n---\n\n## 🥊 vs Adobe's first-party Experience Platform tools\n\nAdobe ships first-party tools through [CX Coworker Gateway](https://experienceleague.adobe.com/en/docs/cx-enterprise-ai/experience-cloud-ai/mcp/overview). It's a genuinely good product, and if all you need is to *ask questions about* your tenant, use it.\n\n| | Adobe AEP tools (CX Coworker Gateway) | @focusgts/aep-mcp-server |\n|---|---|---|\n| Operations | **Read-only** (`search_*`) | **Full CRUD** (read + write) |\n| Tool count | 8 | **61** |\n| Access | **Invitation-only** + org enablement | `npm install` — any org with API credentials |\n| Batch ingestion | Not available | **7 tools** |\n| Profiles / Identity | Not covered | **6 tools** |\n| Privacy Service | Not covered | **6 tools** |\n| Data Lifecycle | Not covered | **9 tools** |\n| Transport | Adobe-hosted gateway | stdio (local, composes with other MCPs) |\n| Data path | Queries traverse Adobe's gateway | Runs entirely in your own VPC |\n| License | Proprietary | **Apache 2.0** |\n| Error responses | — | Structured `AEP_{status}` codes |\n\n> **On Journey Optimizer and CJA:** Adobe ships separate first-party MCP servers for both, so the rows above deliberately do not claim they are \"not covered\" — that would be false. What this server adds is a *single credential* spanning all three, and a write path on the AEP side that Adobe's gateway does not offer. The AJO and CJA tools here are read-only.\n\nAudiences and destinations do appear, but in a *separate* Real-Time CDP tool set on the same gateway — and Adobe is explicit that creating, activating, updating, or deleting audiences, destinations, and dataflows isn't supported there either. The read-only boundary holds across the whole gateway.\n\n**They're complementary, not competing: pair Adobe's gateway for governed reads with this server for the write path.**\n\n> Adobe's figures were read from [their Experience Platform tools page](https://experienceleague.adobe.com/en/docs/cx-enterprise-ai/experience-cloud-ai/mcp/mcp-product-tools/aep-mcp) (last updated 17 July 2026): `search_datasets`, `search_class_relations`, `search_data_access`, `search_data_lake`, `search_dule`, `search_query_service`, `search_audit`, `search_allowed_ip_ranges`. It's a Beta surface and will change — check their docs for the current figure. **The read/write split is the durable difference, not the count.**\n\n---\n\n## 🔌 Add it to your tool\n\n<details open>\n<summary><b>Claude Code</b> — one command</summary>\n\n```bash\nclaude mcp add aep \\\n  -e AEP_CLIENT_ID=... -e AEP_CLIENT_SECRET=... \\\n  -e AEP_ORG_ID=...@AdobeOrg -e AEP_SANDBOX_NAME=your-dev-sandbox \\\n  -- npx -y @focusgts/aep-mcp-server\n```\n</details>\n\n<details>\n<summary><b>Claude Desktop</b> — <code>claude_desktop_config.json</code></summary>\n\n`~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or `%APPDATA%\\Claude\\claude_desktop_config.json` (Windows):\n\n```json\n{\n  \"mcpServers\": {\n    \"aep\": {\n      \"command\": \"npx\",\n      \"args\": [\"-y\", \"@focusgts/aep-mcp-server\"],\n      \"env\": {\n        \"AEP_CLIENT_ID\": \"...\",\n        \"AEP_CLIENT_SECRET\": \"...\",\n        \"AEP_ORG_ID\": \"...@AdobeOrg\",\n        \"AEP_SANDBOX_NAME\": \"your-dev-sandbox\"\n      }\n    }\n  }\n}\n```\n</details>\n\n<details>\n<summary><b>Cursor</b> — <code>.cursor/mcp.json</code></summary>\n\n```json\n{\n  \"mcpServers\": {\n    \"aep\": {\n      \"command\": \"npx\",\n      \"args\": [\"-y\", \"@focusgts/aep-mcp-server\"],\n      \"env\": {\n        \"AEP_CLIENT_ID\": \"...\",\n        \"AEP_CLIENT_SECRET\": \"...\",\n        \"AEP_ORG_ID\": \"...@AdobeOrg\",\n        \"AEP_SANDBOX_NAME\": \"your-dev-sandbox\"\n      }\n    }\n  }\n}\n```\n</details>\n\n<details>\n<summary><b>VS Code (GitHub Copilot)</b> — <code>.vscode/mcp.json</code></summary>\n\n```json\n{\n  \"servers\": {\n    \"aep\": {\n      \"command\": \"npx\",\n      \"args\": [\"-y\", \"@focusgts/aep-mcp-server\"],\n      \"env\": {\n        \"AEP_CLIENT_ID\": \"...\",\n        \"AEP_CLIENT_SECRET\": \"...\",\n        \"AEP_ORG_ID\": \"...@AdobeOrg\",\n        \"AEP_SANDBOX_NAME\": \"your-dev-sandbox\"\n      }\n    }\n  }\n}\n```\n</details>\n\n---\n\n## 🔐 Credentials\n\nGet them at [developer.adobe.com/console](https://developer.adobe.com/console): create a project → add **Experience Platform API** → **OAuth Server-to-Server**.\n\n| Variable | Required | Description |\n|---|---|---|\n| `AEP_CLIENT_ID` | **Yes** | Adobe I/O client ID |\n| `AEP_CLIENT_SECRET` | **Yes** | Adobe I/O client secret |\n| `AEP_ORG_ID` | **Yes** | IMS org ID — must end `@AdobeOrg` |\n| `AEP_SANDBOX_NAME` | **Yes** | Sandbox to scope every call to. **No default** — see below |\n| `AEP_ALLOW_MUTATIONS` | No | `true` to permit any write at all. Off by default |\n| `AEP_MODE` | No | `read-only` · `safe` *(default)* · `production` |\n| `AEP_I_UNDERSTAND_THIS_WRITES_TO_PROD` | No | Only if your sandbox is genuinely *named* `prod` |\n| `AEP_LOG_RESPONSE_BODIES` | No | Log raw Adobe error bodies. Off by default — Adobe echoes request context, which can include identity values |\n| `LOG_LEVEL` | No | Pino level (default `info`) |\n| `AEP_REQUEST_TIMEOUT_MS` | No | Per-request timeout (default `30000`) |\n| `AEP_MAX_RETRIES` | No | Retries on 429/5xx (default `3`) |\n| `CJA_GLOBAL_COMPANY_ID` | No | CJA global company id. Omit it — CJA scopes by IMS org for this credential shape. Set it only to force an explicit `x-proxy-global-company-id` |\n| `CJA_BASE_URL` | No | Override the CJA host (default `https://cja.adobe.io`) |\n| `CJA_REQUEST_TIMEOUT_MS` | No | Per-request timeout for CJA (default `30000`) |\n| `CJA_MAX_RETRIES` | No | CJA retries on 429/5xx (default `3`) |\n| `CJA_LIVE_TESTS` | No | Set to `1` to enable the opt-in live CJA integration tests |\n\n> **`AEP_SANDBOX_NAME` has no default, deliberately.** It used to fall back to `prod`, which meant a config file missing one line silently pointed every request — reads included — at production, with no warning. There is no safe default: a wrong guess is indistinguishable from a correct one until something is read or written in the wrong environment. Setting it explicitly to `prod` is allowed; that's a visible, deliberate choice, and mutations there are still refused by the write guard.\n>\n> **Sandbox scoping.** Every tool sends `x-sandbox-name`, and Query Service derives its database as `<AEP_SANDBOX_NAME>:all`.\n\n---\n\n## 🧾 Entitlements\n\nNot every Adobe org licenses every AEP product. A tool returning `AEP_403` usually means a missing entitlement rather than a bad credential.\n\n| Category | Required entitlement |\n|---|---|\n| Schemas · Datasets · Ingestion | AEP (base) |\n| Identities | AEP (base) + Identity Service |\n| Profiles · Segments · Destinations | Real-Time CDP |\n| Sources | AEP (base) — connector availability varies by SKU |\n| Query Service | AEP Query Service add-on |\n| Privacy Service | Adobe Privacy Service (sold separately) |\n| Data Hygiene | AEP (base). Adobe documents **no** Data Distiller gate here — an earlier version of this table wrongly claimed one. A `401` means wrong org, wrong sandbox, or wrong credential profile, in that order |\n\n---\n\n## 🏗️ Architecture\n\nTypeScript `strict` end-to-end, `@modelcontextprotocol/sdk` + `zod`, stdio transport, stateless per request.\n\n```mermaid\nflowchart LR\n  C[\"MCP client<br/>Claude · Cursor<br/>Copilot · ChatGPT\"]\n  T[\"aep-mcp-server<br/><b>61 tools</b><br/>14 categories\"]\n  G{{\"write guard<br/>fail-closed\"}}\n  A1[\"Schema Registry<br/>· Catalog\"]\n  A2[\"Batch Ingestion\"]\n  A3[\"UPS · Segmentation<br/>· Destinations\"]\n  A4[\"Data Lifecycle<br/>· Privacy\"]\n  IMS[/\"Adobe IMS<br/>OAuth S2S\"/]\n\n  C -- \"stdio · JSON-RPC 2.0\" --> T\n  T -- \"every call, no exceptions\" --> G\n  IMS -. \"token cache · 401 re-auth\" .-> T\n  G -- \"HTTPS · Bearer · x-sandbox-name\" --> A1\n  G --> A2\n  G --> A3\n  G --> A4\n```\n\nOAuth Server-to-Server with a deduped token cache, structured pino logging with PII redaction, exponential-backoff retries, automatic 401 re-auth, working cursor pagination, structured `AEP_{status}` error codes, and a graceful-shutdown lifecycle. All logs go to **stderr** — stdout is reserved for the MCP JSON-RPC stream.\n\n---\n\n## 🧪 Development\n\n```bash\ngit clone https://github.com/Focus-GTS/aep-mcp-server.git\ncd aep-mcp-server && npm install && npm run build && npm test\n```\n\n```bash\nnpm run dev          # tsx src/server.ts (hot-reload)\nnpm test             # vitest — 509 tests\nnpm run typecheck    # tsc --noEmit\nnpm run tools        # print the registered tool surface\n```\n\n`npm run test:live` runs a read-only smoke suite against a real IMS org and sandbox to verify credentials, entitlements, and sandbox scoping end to end. It invokes **no** destructive tool and requires `AEP_SANDBOX_NAME` to point at a non-production sandbox.\n\n---\n\n## 🧩 Part of the Focus GTS Adobe suite\n\n| | |\n|---|---|\n| [eds-mcp-server](https://github.com/Focus-GTS/eds-mcp-server) | MCP server for Adobe Edge Delivery Services — read, audit, fix, publish and undo your site |\n| [eds-content-ops-skills](https://github.com/Focus-GTS/eds-content-ops-skills) | AI skills for EDS content ops — first third-party contributor merged into [Adobe's official skills repo](https://github.com/adobe/skills) |\n| [eds-ops](https://github.com/Focus-GTS/eds-ops) | CLI + GitHub Action for automated site grading and PR gating |\n| [EDS Score](https://www.focusgts.com/eds-score/) | Free browser-based site health analyzer |\n\n---\n\n<div align=\"center\">\n\nBuilt by **[Focus GTS](https://focusgts.com)** — Adobe Silver Solution Partner · Apache-2.0\n<br/>Bug reports and PRs welcome at [Focus-GTS/aep-mcp-server](https://github.com/Focus-GTS/aep-mcp-server/issues) · <dfox@focusgts.com>\n<br/>Not affiliated with or endorsed by Adobe Inc. or Anthropic, PBC.\n\n</div>\n",
  "bytes": 26095,
  "sha": "8baa115fc7d4ce699f0e591e5f6b1e782902e95ce1073c6664710b80e39c20d0",
  "repo_slug": "focus-gts/aep-mcp-server",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_com_focusgts_aep_2349ae9f/readme"
}