Skip to main content
Glama
mercuryadvising

Adobe Launch / Tags MCP Server

README.md
# Adobe Launch / Tags MCP Server

An MCP server wrapping the [Adobe Experience Platform Reactor API](https://experienceleague.adobe.com/en/docs/experience-platform/tags/api/overview) — the API behind Adobe Launch, now called Adobe Experience Platform Tags.

It gives an assistant read access to your tag properties, rules, data elements, extensions, libraries and environments, plus the ability to author changes. **It does not publish anything by default.** Nothing this server does reaches a live site unless you explicitly enable `dangerous` mode and approve a build.

Presenting this to someone? See [DEMO.md](DEMO.md) for a 10-minute walkthrough.

---

## Why this exists

The Reactor API is a JSON:API surface with ~240 endpoints and a few sharp edges that make it awkward to hand to a model raw:

- Delegate configuration is stored in a `settings` attribute that is a **JSON-encoded string**, not an object. Passing an object returns a cryptic 422.
- Resources are fat — a rule component carries about 20 audit fields — so a list of 200 rules will swamp a context window.
- Rules are meaningless without their rule components, which live at a separate endpoint.
- Authoring and publishing are entirely separate concerns, and it is easy to conflate them.

This wrapper handles all four: it encodes and decodes `settings`, shapes responses down to what matters, joins rules to their components, and gates publishing behind an explicit opt-in.

---

## Setup

### 1. Create an OAuth Server-to-Server credential

Adobe retired the Service Account (JWT) flow on 1 January 2025. You need an OAuth Server-to-Server credential.

1. Go to the [Adobe Developer Console](https://developer.adobe.com/console) and sign in.
2. Create a new project (or open an existing one).
3. **Add API** → select **Adobe Experience Platform Launch API** (listed under Experience Cloud).
4. Choose **OAuth Server-to-Server** as the authentication type.
5. Select the product profile(s) that grant access to the properties you care about. Product profiles are managed in the Adobe Admin Console; if you are not an admin, ask yours which profile covers your properties. **This is the single most common cause of empty results or 403s** — the credential can authenticate fine and still see nothing.
6. From the credential's page, copy:
   - **Client ID** → `REACTOR_CLIENT_ID`
   - **Client Secret** → `REACTOR_CLIENT_SECRET`
   - **Organization ID** (ends in `@AdobeOrg`) → `REACTOR_ORG_ID`
   - **Scopes** (the comma-separated list shown on the credential page) → `REACTOR_SCOPES`

The server exchanges these for a 24-hour access token at `https://ims-na1.adobelogin.com/ims/token/v3` and refreshes it automatically. You never handle a token yourself.

### 2. Install

A prebuilt, dependency-free bundle ships at `build/adobe-launch-reactor-mcp.mjs`. It is a single
self-contained ESM file — **no `npm install` required** to run the server. Node 20+ is the only
prerequisite.

To rebuild it from source, or to run the tests:

```bash
npm install
npm run build     # tsc -> dist/
npm run bundle    # single file -> build/adobe-launch-reactor-mcp.mjs
npm test
```

> The server reads its credentials from **environment variables supplied by your MCP client**.
> It does not load a `.env` file — `.env.example` is a reference for which values to set.

### 3. Configure your MCP client

**Claude Desktop** — `claude_desktop_config.json`:

```json
{
  "mcpServers": {
    "adobe-launch": {
      "command": "node",
      "args": ["/absolute/path/to/launch-mcp/build/adobe-launch-reactor-mcp.mjs"],
      "env": {
        "REACTOR_CLIENT_ID": "your-client-id",
        "REACTOR_CLIENT_SECRET": "your-client-secret",
        "REACTOR_ORG_ID": "XXXXXXXXXXXX@AdobeOrg",
        "REACTOR_SCOPES": "openid,AdobeID,read_organizations,additional_info.projectedProductContext,additional_info.roles,session",
        "REACTOR_WRITE_MODE": "read"
      }
    }
  }
}
```

**Claude Code** — `claude mcp add`:

```bash
claude mcp add adobe-launch \
  --env REACTOR_CLIENT_ID=... \
  --env REACTOR_CLIENT_SECRET=... \
  --env REACTOR_ORG_ID=...@AdobeOrg \
  --env REACTOR_WRITE_MODE=safe \
  -- node /absolute/path/to/launch-mcp/build/adobe-launch-reactor-mcp.mjs
```

Then ask: *"What rules are in my Acme Web property, and which ones are unpublished?"*

---

## Configuration

| Variable | Required | Default | Purpose |
| --- | --- | --- | --- |
| `REACTOR_CLIENT_ID` | yes | — | Client ID from Developer Console; also sent as `x-api-key`. |
| `REACTOR_CLIENT_SECRET` | yes | — | Client secret. |
| `REACTOR_ORG_ID` | yes | — | IMS org ID, ends in `@AdobeOrg`. |
| `REACTOR_SCOPES` | no | `openid,AdobeID,read_organizations,additional_info.projectedProductContext,additional_info.roles,session` | Comma-separated scopes. **Copy the exact list from your credential page** — the default is typical but not universal. |
| `REACTOR_WRITE_MODE` | no | `read` | `read`, `safe`, or `dangerous`. See below. |
| `REACTOR_DEFAULT_PROPERTY_ID` | no | — | Pin a property so tools don't need `property_id` each call. |
| `REACTOR_DEFAULT_COMPANY_ID` | no | — | Pin a company, skipping a lookup call. |
| `REACTOR_BASE_URL` | no | `https://reactor.adobe.io` | Override for testing. |
| `REACTOR_IMS_TOKEN_URL` | no | `https://ims-na1.adobelogin.com/ims/token/v3` | Change for non-NA1 IMS regions. |
| `REACTOR_MAX_RESPONSE_CHARS` | no | `60000` | Cap on a single tool response before truncation. |
| `REACTOR_TIMEOUT_MS` | no | `30000` | Per-request timeout. |

### Write modes

| Mode | Can do | Cannot do |
| --- | --- | --- |
| `read` (default) | List and get everything, search, read audit events | Any mutation |
| `safe` | Everything in `read`, plus create/update rules, rule components, data elements, extensions, libraries, notes; assemble library contents | Delete, build, publish |
| `dangerous` | Everything, including deletes, `reactor_create_build` and `reactor_transition_library` (submit/approve/publish) | — |

The mode is enforced in two places: write tools are simply not registered below their tier, and the `reactor_request` escape hatch whitelists HTTP methods by mode — so a model cannot route around the tier by calling the raw endpoint.

`safe` is the right default for most work. Authoring in Reactor is non-destructive: a new or edited rule sits in the property's authoring state and changes nothing on a live site until a library containing it is built and published. Keeping publishing in a separate, explicitly-enabled tier means a mistake is something you fix in the UI, not an incident.

---

## Tools

### Read (all modes)

| Tool | What it does |
| --- | --- |
| `reactor_whoami` | Profile + visible companies. Start here to verify auth. |
| `reactor_list_properties` | Properties in a company. |
| `reactor_get_property` | One property with its platform settings. |
| `reactor_list_rules` | Rules in a property. |
| `reactor_get_rule` | A rule **plus its components** — use this to answer "what does this rule do?". |
| `reactor_list_rule_components` | Events, conditions and actions of a rule. |
| `reactor_list_data_elements` / `reactor_get_data_element` | Data elements, with settings decoded. |
| `reactor_list_extensions` / `reactor_get_extension` | Installed extensions and their configuration. |
| `reactor_list_extension_packages` | The catalog of installable extensions. |
| `reactor_get_extension_package` | Delegate descriptors an extension provides — how you discover valid `delegate_descriptor_id` values. |
| `reactor_list_libraries` | Libraries and their workflow state. |
| `reactor_get_library` | A library plus every rule, data element and extension it contains, and its last build. |
| `reactor_list_builds` | Build history for a library. |
| `reactor_list_environments` | Environments with embed codes. |
| `reactor_list_hosts` | Akamai / SFTP hosts. |
| `reactor_list_audit_events` | Who changed what, newest first. |
| `reactor_search` | Structured search across resource types when you don't know which property something lives in. |
| `reactor_list_notes` | Notes attached to a resource. |
| `reactor_request` | Raw passthrough for the ~200 endpoints without a dedicated tool. Method-gated by write mode. |

### Authoring (`safe` and `dangerous`)

`reactor_create_rule`, `reactor_update_rule`, `reactor_create_rule_component`, `reactor_update_rule_component`, `reactor_create_data_element`, `reactor_update_data_element`, `reactor_install_extension`, `reactor_update_extension`, `reactor_create_library`, `reactor_update_library`, `reactor_library_add_resources`, `reactor_library_remove_resources`, `reactor_create_note`.

### Destructive (`dangerous` only)

`reactor_create_build`, `reactor_transition_library`, `reactor_delete_resource`. All three require an explicit `confirm: true` argument and carry the MCP `destructiveHint` annotation, so a well-behaved client will prompt before running them.

---

## Notes on the API

**Filtering.** List tools take a `filter` object keyed by attribute, with values of the form `"OPERATOR value"`. Operators: `EQ`, `NOT`, `LT`, `GT`, `BETWEEN`, `CONTAINS`.

```json
{ "filter": { "name": "CONTAINS checkout", "enabled": "EQ true" } }
```

Adobe **silently ignores filters on non-filterable attributes** and returns the full set, so a filter that appears to do nothing is usually a typo or an unsupported attribute rather than a genuinely empty result.

Filterable attributes by resource:

- **rules**, **rule_components**, **data_elements**: `name`, `enabled`, `dirty`, `published`, `published_at`, `revision_number`, `origin_id`, `created_at`, `updated_at` (rule components also `negate`)
- **properties**: `name`, `platform`, `enabled`, `token`, `created_at`, `updated_at`
- **extensions**: `name`, `display_name`, `version`, `enabled`, `dirty`, `published`, `revision_number`, `created_at`, `updated_at`
- **libraries**: `name`, `state`, `stale`, `published_at`, `created_at`, `updated_at`
- **environments**: `name`, `stage`, `archive`, `token`, `created_at`, `updated_at`
- **builds**: `status`, `token`, `created_at`, `updated_at`
- **hosts**: `name`, `type_of`, `created_at`, `updated_at`

**Pagination.** 25 records per page by default, 100 max. Pass `fetch_all: true` to walk every page (capped at 20 pages). Responses larger than `REACTOR_MAX_RESPONSE_CHARS` are truncated at a record boundary, so the JSON stays parseable and says how much was dropped.

**Rate limits.** Reactor's soft limit is roughly 120 requests per minute per integration. The client retries `429` and `5xx` with exponential backoff, honouring `Retry-After`.

**Settings.** Reactor stores delegate configuration as a JSON-encoded string. The write tools accept a plain object and encode it; the read tools decode it back. If you pass a string it goes through unchanged.

**Publishing model.** Authoring changes are inert. To make a change live: create or edit resources → add them to a library → build the library → transition it to `published`. The last two steps require `dangerous` mode; otherwise do them in the Launch UI.

---

## Development

```bash
npm run typecheck   # tsc --noEmit
npm test            # 35 tests against an in-memory mock of IMS + Reactor
npm run build       # compile to dist/
npm run dev         # run from source via tsx
```

The test suite covers auth header construction and token caching, the client_credentials grant shape, JSON:API filter/sort/page encoding, multi-page walking, response shaping and truncation, `settings` encode/decode round-tripping, relationship construction, write-mode gating (including that the raw passthrough cannot be used to escape it), and error message quality for 404/422/429/500.

## Structure

```
src/
  config.ts        env parsing, write-mode tiers
  auth.ts          IMS OAuth S2S token manager with caching + refresh collapsing
  client.ts        JSON:API client: headers, query encoding, retry, pagination, error formatting
  format.ts        response shaping and size-capped serialization
  server.ts        MCP server assembly
  index.ts         stdio entrypoint
  tools/
    common.ts      shared argument schemas and helpers
    read.ts        read-only tools
    write.ts       authoring tools + dangerous tier
    raw.ts         method-gated passthrough
```

## Licence

MIT.