com.focusgts/aep
Full-CRUD server for AEP, Journey Optimizer and CJA. 61 tools from one OAuth credential.
Open source Open in the app JSON README (API)
About
Full-CRUD server for AEP, Journey Optimizer and CJA. 61 tools from one OAuth credential.
Details
- Kind
- MCP servers
- Topic
- Security & identity
- Publisher
- com.focusgts
- Origin
- official
- Category
- ferramentas
- Transport
- local
- Version
- 0.11.1
- Last push
- 2026-08-30T19:47:38Z
- Repository state
- ativo
- Language
- TypeScript
- License
- Apache-2.0
- Added
- 2026-08-29 03:01:07
- Updated
- 2026-08-30 20:01:02
- Origin id
com.focusgts/aep
README
<div align="center">

[](https://www.npmjs.com/package/@focusgts/aep-mcp-server)
[](https://www.npmjs.com/package/@focusgts/aep-mcp-server)
[](https://registry.modelcontextprotocol.io)
[](#-development)
[](LICENSE)
### Adobe's MCP lets your agent *read* Experience Platform. This one lets it **work**.
**61 tools across 14 categories. Full read AND write. Self-hosted, Apache-2.0, no invitation required.**
Experience Platform, Journey Optimizer and Customer Journey Analytics — from one OAuth credential.
**Ingest a batch → compose a schema → activate an audience → honour an erasure.**
Every mutation gated by a fail-closed write guard that asks Adobe what kind of sandbox it's in.

</div>
---
## ⚡ Do it in three lines
```bash
claude mcp add aep \
-e AEP_CLIENT_ID=... -e AEP_CLIENT_SECRET=... \
-e AEP_ORG_ID=...@AdobeOrg -e AEP_SANDBOX_NAME=your-dev-sandbox \
-- npx -y @focusgts/aep-mcp-server
```
Then just ask your agent:
> *"Create a schema with the Demographic Details field group, then a dataset on it."*
> *"Ingest this NDJSON file and tell me when the batch lands."*
> *"Build an audience of customers who bought twice this quarter and activate it."*
> *"Delete every record for this email address — dry run first."*
Writes are off until you ask for them, and `safe` mode only unlocks sandboxes **Adobe** classifies as development.
### The loop that makes it different
```mermaid
flowchart LR
A["📐 Compose<br/>schema from field groups"] --> B["🗂️ Create<br/>dataset"]
B --> C["📥 Ingest<br/>batch · upload · complete"]
C --> D["🎯 Activate<br/>segment → destination"]
D --> E["🧹 Govern<br/>erasure · expiration · quota"]
E -. "re-audit the tenant" .-> A
```
Adobe'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.
---
## 🧠 How it works
```mermaid
flowchart LR
A["AI agent<br/>(Claude · Cursor · Copilot)"] -- MCP / stdio --> B["aep-mcp-server<br/>61 tools"]
B --> W{{"write guard<br/>fail-closed"}}
W --> C["Schema Registry · Catalog<br/>Ingestion · Lifecycle · Privacy"]
C --> F["Your AEP sandbox<br/>platform.adobe.io"]
B --> J["Journey Optimizer<br/>read-only"]
J --> K["ajo campaigns"]
B --> Q["Customer Journey Analytics<br/>read-only, no sandbox"]
Q --> L["cja.adobe.io"]
```
The 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.
---
## 🛡️ Safe by default
Three postures. **Reads are never restricted in any mode.**
| `AEP_MODE` | Writes permitted | Use it when |
|---|---|---|
| `read-only` | Never, in any sandbox | Handing the server to someone to explore an environment you don't want touched |
| **`safe`** *(default)* | Only where Adobe classifies the sandbox `development` | Evaluating, or letting an agent work without risking production |
| `production` | Anywhere, including production | You run your own change control and don't want the server second-guessing you |
> **How `safe` decides — and why it's not the sandbox name.**
> 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.
>
> **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.
>
> **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.
>
> **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".
Startup always states the active posture:
```
SAFE MODE — sandbox is a development sandbox, so writes are ENABLED.
SAFE MODE — sandbox is PRODUCTION, so writes are BLOCKED. Reads work normally.
SAFE MODE — sandbox type could not be confirmed, so writes are BLOCKED (fail-closed).
READ-ONLY MODE — no write, update, or delete will be performed in any sandbox.
PRODUCTION MODE — writes permitted against ANY sandbox, including production.
```
### Per-tool confirmation gates
Writes 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**:
| Tool | Gate |
|---|---|
| `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**. |
| `aep_delete_segment` | `confirm: "DELETE SEGMENT <segmentId>"` — segments were create-only until 0.9.1, so every one an agent made was permanent |
| `aep_delete_dataset` | `confirm: "DELETE DATASET <id>"`, escalating to `"DELETE PROFILE-ENABLED DATASET <id>"` when the dataset feeds Profile |
| `aep_complete_batch` | `confirm: "COMPLETE BATCH <batchId>"` — the point of no return for ingestion |
| `aep_revert_batch` | `confirm: "REVERT BATCH <batchId>"` |
| `aep_create_dataset_expiration` | `confirm: "CREATE DATASET EXPIRATION <datasetId>"` — unless `dryRun: true` |
| `aep_update_dataset_expiration` | `confirm: "UPDATE DATASET EXPIRATION <ttlId>"` |
| `aep_cancel_dataset_expiration` | `confirm: "CANCEL DATASET EXPIRATION <ttlId>"` |
| `aep_delete_profile` | `confirm: "I understand this is irreversible"` *(deprecated — prefer Data Hygiene)* |
> **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.
>
> **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.
>
> 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.
### Tool annotations
Every tool ships MCP annotations — `readOnlyHint`, `destructiveHint`, `idempotentHint`, `openWorldHint` — derived from the same metadata that builds its description, so the two cannot drift.
| | Count |
|---|---|
| `readOnlyHint: true` | 39 |
| `destructiveHint: true` | 8 |
| Un-annotated | **0** |
These 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.
---
## 🛠️ The 61 tools
All prefixed `aep_`, `verb_noun` naming. 🔒 changes state · 🔥 destructive.
### Data modelling & ingestion
<table>
<tr><td valign="top" width="33%">
**Schemas** (4)
- `list_schemas`
- `get_schema`
- `create_schema` 🔒
- `update_schema` 🔒
**Datasets** (4)
- `list_datasets`
- `get_dataset`
- `create_dataset` 🔒
- `delete_dataset` 🔥
</td><td valign="top" width="33%">
**Ingestion** (7)
- `create_batch` 🔒
- `upload_batch_file` 🔒
- `complete_batch` 🔥
- `get_batch_status`
- `list_batches`
- `abort_batch` 🔥
- `revert_batch` 🔥
</td><td valign="top" width="33%">
**Sources** (2)
- `list_sources`
- `list_dataflows`
**Query Service** (3)
- `run_query` 🔒
- `get_query_status`
- `list_queries`
</td></tr>
</table>
### Profiles, audiences & activation
<table>
<tr><td valign="top" width="33%">
**Identities** (2)
- `list_identity_namespaces`
- `get_identity_graph`
</td><td valign="top" width="33%">
**Profiles** (4)
- `get_profile`
- `get_profile_by_identity`
- `preview_profile`
- `delete_profile` 🔥
</td><td valign="top" width="33%">
**Segments** (5)
- `list_segments`
- `get_segment`
- `create_segment` 🔒
- `estimate_segment_size`
- `delete_segment` 🔥
**Destinations** (3)
- `list_destinations`
- `create_destination_connection` 🔒
- `activate_segment` 🔒
</td></tr>
</table>
### Governance — privacy, lifecycle & event routing
<table>
<tr><td valign="top" width="33%">
**Privacy Service** (6)
- `create_privacy_job` 🔒
- `get_privacy_job`
- `list_privacy_jobs`
- `cancel_privacy_job` 🔒
- `get_privacy_job_results`
- `list_privacy_namespaces`
</td><td valign="top" width="33%">
**Data Hygiene** (9)
- `create_record_delete` 🔥
- `get_work_order_status`
- `list_work_orders`
- `get_data_lifecycle_quota`
- `create_dataset_expiration` 🔥
- `get_dataset_expiration`
- `list_dataset_expirations`
- `update_dataset_expiration` 🔥
- `cancel_dataset_expiration` 🔥
</td></tr>
</table>
> **Workflows these unlock**
>
> **Ingest end to end** — `create_schema` → `create_dataset` → `create_batch` → `upload_batch_file` → `complete_batch` → `get_batch_status`
>
> **Build and activate an audience** — `create_segment` → `estimate_segment_size` → `list_destinations` → `create_destination_connection` → `activate_segment`
>
> **Honour an erasure request** — `get_profile_by_identity` → `create_record_delete` → `get_work_order_status`
>
> **Retire data on a schedule** — `create_dataset_expiration` → `list_dataset_expirations` → `update_dataset_expiration` → `cancel_dataset_expiration`
**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.
### Adobe Journey Optimizer (2)
AJO is a **separate Adobe product, licensed separately** — hence the `ajo_` prefix, so an entitlement failure reads as one.
<table>
<tr><td valign="top" width="50%">
**Campaigns**
- `ajo_list_campaigns`
- `ajo_get_campaign`
</td><td valign="top" width="50%">
Campaigns 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.
</td></tr>
</table>
> 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.
---
## 📊 AEC-Bench — does your agent actually work?
Every 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.**
`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?
```bash
npm run bench # tier 1, read-only, safe on any tenant
npm run bench:write # tier 2, creates and removes what it creates
```
| | |
|---|---|
| **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. |
| **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. |
| **Tier 1 is production-safe** | GET only. A benchmark nobody dares run measures nothing. |
Current: **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.
We expect to score badly on tasks we haven't built for. That's the intended use.
## 📈 Customer Journey Analytics (10)
**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.
<table>
<tr><td valign="top" width="50%">
**Discover**
- `cja_list_companies`
- `cja_list_connections`
- `cja_get_connection`
- `cja_list_data_views`
- `cja_get_data_view`
</td><td valign="top" width="50%">
**Report**
- `cja_list_dimensions`
- `cja_list_metrics`
- `cja_list_segments`
- `cja_list_calculated_metrics`
- `cja_run_report`
</td></tr>
</table>
### The AEP sandbox and the CJA company are not the same thing
| | AEP | CJA |
|---|---|---|
| Host | `platform.adobe.io` | `cja.adobe.io` |
| Scope unit | **sandbox** (`AEP_SANDBOX_NAME`) | **global company id** — or the IMS org |
| Header | `x-sandbox-name` | `x-proxy-global-company-id` *(optional — see below)* |
| Maps to the other? | **No.** A CJA connection or data view has no one-to-one relationship with any AEP sandbox. |
The 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.
### Company discovery, and why it may not work
Adobe'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.
In practice this blocks nothing: **CJA answers every resource with `x-gw-ims-org-id` alone**, so the company id is optional.
```bash
# Optional. Omit it and CJA scopes by IMS org, which is what works by default.
CJA_GLOBAL_COMPANY_ID=your-global-company-id
```
`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.
### Running the probes and the live tests
```bash
node scripts/probe-cja.mjs --env .env # read-only, sanitized output
CJA_LIVE_TESTS=1 npm test -- tests/integration/cja-live.test.ts
```
The 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.
### Current limitations, honestly
- **No mutation tools.** This slice is read-only by design.
- **Discovery is unavailable** on a CJA-only credential, as above. Not a defect in the tools.
- **`cja_get_connection` is unvalidated** — the validation tenant has zero connections, and an id is never fabricated to manufacture a pass.
- **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.
---
## 🥊 vs Adobe's first-party Experience Platform tools
Adobe 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.
| | Adobe AEP tools (CX Coworker Gateway) | @focusgts/aep-mcp-server |
|---|---|---|
| Operations | **Read-only** (`search_*`) | **Full CRUD** (read + write) |
| Tool count | 8 | **61** |
| Access | **Invitation-only** + org enablement | `npm install` — any org with API credentials |
| Batch ingestion | Not available | **7 tools** |
| Profiles / Identity | Not covered | **6 tools** |
| Privacy Service | Not covered | **6 tools** |
| Data Lifecycle | Not covered | **9 tools** |
| Transport | Adobe-hosted gateway | stdio (local, composes with other MCPs) |
| Data path | Queries traverse Adobe's gateway | Runs entirely in your own VPC |
| License | Proprietary | **Apache 2.0** |
| Error responses | — | Structured `AEP_{status}` codes |
> **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.
Audiences 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.
**They're complementary, not competing: pair Adobe's gateway for governed reads with this server for the write path.**
> 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.**
---
## 🔌 Add it to your tool
<details open>
<summary><b>Claude Code</b> — one command</summary>
```bash
claude mcp add aep \
-e AEP_CLIENT_ID=... -e AEP_CLIENT_SECRET=... \
-e AEP_ORG_ID=...@AdobeOrg -e AEP_SANDBOX_NAME=your-dev-sandbox \
-- npx -y @focusgts/aep-mcp-server
```
</details>
<details>
<summary><b>Claude Desktop</b> — <code>claude_desktop_config.json</code></summary>
`~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or `%APPDATA%\Claude\claude_desktop_config.json` (Windows):
```json
{
"mcpServers": {
"aep": {
"command": "npx",
"args": ["-y", "@focusgts/aep-mcp-server"],
"env": {
"AEP_CLIENT_ID": "...",
"AEP_CLIENT_SECRET": "...",
"AEP_ORG_ID": "...@AdobeOrg",
"AEP_SANDBOX_NAME": "your-dev-sandbox"
}
}
}
}
```
</details>
<details>
<summary><b>Cursor</b> — <code>.cursor/mcp.json</code></summary>
```json
{
"mcpServers": {
"aep": {
"command": "npx",
"args": ["-y", "@focusgts/aep-mcp-server"],
"env": {
"AEP_CLIENT_ID": "...",
"AEP_CLIENT_SECRET": "...",
"AEP_ORG_ID": "...@AdobeOrg",
"AEP_SANDBOX_NAME": "your-dev-sandbox"
}
}
}
}
```
</details>
<details>
<summary><b>VS Code (GitHub Copilot)</b> — <code>.vscode/mcp.json</code></summary>
```json
{
"servers": {
"aep": {
"command": "npx",
"args": ["-y", "@focusgts/aep-mcp-server"],
"env": {
"AEP_CLIENT_ID": "...",
"AEP_CLIENT_SECRET": "...",
"AEP_ORG_ID": "...@AdobeOrg",
"AEP_SANDBOX_NAME": "your-dev-sandbox"
}
}
}
}
```
</details>
---
## 🔐 Credentials
Get them at [developer.adobe.com/console](https://developer.adobe.com/console): create a project → add **Experience Platform API** → **OAuth Server-to-Server**.
| Variable | Required | Description |
|---|---|---|
| `AEP_CLIENT_ID` | **Yes** | Adobe I/O client ID |
| `AEP_CLIENT_SECRET` | **Yes** | Adobe I/O client secret |
| `AEP_ORG_ID` | **Yes** | IMS org ID — must end `@AdobeOrg` |
| `AEP_SANDBOX_NAME` | **Yes** | Sandbox to scope every call to. **No default** — see below |
| `AEP_ALLOW_MUTATIONS` | No | `true` to permit any write at all. Off by default |
| `AEP_MODE` | No | `read-only` · `safe` *(default)* · `production` |
| `AEP_I_UNDERSTAND_THIS_WRITES_TO_PROD` | No | Only if your sandbox is genuinely *named* `prod` |
| `AEP_LOG_RESPONSE_BODIES` | No | Log raw Adobe error bodies. Off by default — Adobe echoes request context, which can include identity values |
| `LOG_LEVEL` | No | Pino level (default `info`) |
| `AEP_REQUEST_TIMEOUT_MS` | No | Per-request timeout (default `30000`) |
| `AEP_MAX_RETRIES` | No | Retries on 429/5xx (default `3`) |
| `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` |
| `CJA_BASE_URL` | No | Override the CJA host (default `https://cja.adobe.io`) |
| `CJA_REQUEST_TIMEOUT_MS` | No | Per-request timeout for CJA (default `30000`) |
| `CJA_MAX_RETRIES` | No | CJA retries on 429/5xx (default `3`) |
| `CJA_LIVE_TESTS` | No | Set to `1` to enable the opt-in live CJA integration tests |
> **`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.
>
> **Sandbox scoping.** Every tool sends `x-sandbox-name`, and Query Service derives its database as `<AEP_SANDBOX_NAME>:all`.
---
## 🧾 Entitlements
Not every Adobe org licenses every AEP product. A tool returning `AEP_403` usually means a missing entitlement rather than a bad credential.
| Category | Required entitlement |
|---|---|
| Schemas · Datasets · Ingestion | AEP (base) |
| Identities | AEP (base) + Identity Service |
| Profiles · Segments · Destinations | Real-Time CDP |
| Sources | AEP (base) — connector availability varies by SKU |
| Query Service | AEP Query Service add-on |
| Privacy Service | Adobe Privacy Service (sold separately) |
| 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 |
---
## 🏗️ Architecture
TypeScript `strict` end-to-end, `@modelcontextprotocol/sdk` + `zod`, stdio transport, stateless per request.
```mermaid
flowchart LR
C["MCP client<br/>Claude · Cursor<br/>Copilot · ChatGPT"]
T["aep-mcp-server<br/><b>61 tools</b><br/>14 categories"]
G{{"write guard<br/>fail-closed"}}
A1["Schema Registry<br/>· Catalog"]
A2["Batch Ingestion"]
A3["UPS · Segmentation<br/>· Destinations"]
A4["Data Lifecycle<br/>· Privacy"]
IMS[/"Adobe IMS<br/>OAuth S2S"/]
C -- "stdio · JSON-RPC 2.0" --> T
T -- "every call, no exceptions" --> G
IMS -. "token cache · 401 re-auth" .-> T
G -- "HTTPS · Bearer · x-sandbox-name" --> A1
G --> A2
G --> A3
G --> A4
```
OAuth 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.
---
## 🧪 Development
```bash
git clone https://github.com/Focus-GTS/aep-mcp-server.git
cd aep-mcp-server && npm install && npm run build && npm test
```
```bash
npm run dev # tsx src/server.ts (hot-reload)
npm test # vitest — 509 tests
npm run typecheck # tsc --noEmit
npm run tools # print the registered tool surface
```
`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.
---
## 🧩 Part of the Focus GTS Adobe suite
| | |
|---|---|
| [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 |
| [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) |
| [eds-ops](https://github.com/Focus-GTS/eds-ops) | CLI + GitHub Action for automated site grading and PR gating |
| [EDS Score](https://www.focusgts.com/eds-score/) | Free browser-based site health analyzer |
---
<div align="center">
Built by **[Focus GTS](https://focusgts.com)** — Adobe Silver Solution Partner · Apache-2.0
<br/>Bug reports and PRs welcome at [Focus-GTS/aep-mcp-server](https://github.com/Focus-GTS/aep-mcp-server/issues) · <dfox@focusgts.com>
<br/>Not affiliated with or endorsed by Adobe Inc. or Anthropic, PBC.
</div>