Releases: Focus-GTS/aep-mcp-server
Release list
v0.11.1
Fixed
-
v0.11.0 published to npm but was REFUSED by the MCP Registry. The registry
capsserver.jsondescription at 100 characters and returns a 422; the
0.11.0 description was 121. npm and GitHub took it, the registry did not, so
the listing stayed on 0.10.0 while npm moved to 0.11.0.The publish step is
continue-on-error— a registry outage must not fail a
release that already published — so it still showed a green tick. The only
evidence was in the step log. That is the second time a "✓" on that step has
meant nothing; the first was reading its summary line instead of its log.npm run check:countsnow asserts the length locally, so this fails before a
tag rather than after one.
v0.11.0
Added
-
Customer Journey Analytics — 10 read-only tools. CJA is a sibling service
to AEP, not another AEP endpoint: its own host, its own paths, and no
sandbox. It shares only the OAuth credential, so adding the CJA API to the
existing Developer Console project is all that is required. Every path was
established by live probe — CJA's are not uniform, and/data/segments,
/data/calculatedmetricsand/data/reportsare all HTML 404s.Company discovery is unavailable on a CJA-only credential: Adobe serves it
fromanalytics.adobe.io, which belongs to Adobe Analytics, a different
product. CJA answers everything withx-gw-ims-org-idalone, so the company
id is optional viaCJA_GLOBAL_COMPANY_ID. There is no auto-discovery
fallback and no auto-selection — if discovery ever returns several companies,
the tool refuses to pick rather than silently pointing reports at the wrong one. -
A machine-recorded evidence ledger (
docs/validation-evidence.json) and a
test that fails the build if the validation matrix marks a tool live without a
recorded pass.The matrix was hand-maintained: a ✅ meant someone ran something once and typed
a tick. That gap was not theoretical. CJA was reported as live-validated on the
strength of a throwaway script in/tmp, and the read-only sweep had never
constructed a CJA client, so it had never tested a single CJA tool. -
npm run validate:writes— replays the whole write path and removes what
it made: schema → dataset → batch → abort, a second batch through
complete → revert, expiration create/read/update/cancel, then
GET-verified dataset deletion.aep_complete_batchandaep_revert_batch
were previously provable only by an August phase run that nothing replayed —
the most destructive tools in the server holding the weakest evidence.
Fixed
- The read-only sweep never built a CJA client, so all ten CJA tools returned
CJA_NOT_CONFIGURED— five reported as failures that were defects in the
harness, not the product. CjaClientsent a whitespace-onlyx-proxy-global-company-idwhen the
override was blank; the env path trimmed, the constructor path did not.- An HTML 404 from CJA surfaced as "AEP API returned 404" rather than saying the
route does not exist.
Added
-
npm run check:counts— asserts the tool count agrees across all six
places it appears:package.json,server.json, the README, the validation
matrix, and the GitHub repository description. Wired into the release gate.It has now drifted twice. 0.8.0 shipped with every surface saying 46 while the
registry held 53. 0.10.0 left the GitHub About saying 53 while the registry
held 51 — the About is not in the repo, so it is the one that gets forgotten,
and both times it was caught by a person happening to read a page. That is not
a control.A test already keeps the README's tool names honest against the registry;
this covers the counts. The registry is the only source of truth.Its own first run reported a failure that was not one — the README carries
legitimate per-category counts ("7 tools", "9 tools") in the comparison table,
which a bare/(\d+) tools/matches too. It now matches only total-claim
phrasings, and that reasoning is a comment in the file.
Fixed
- The GitHub repository description said 53 tools; the registry holds 51.
Corrected, and now guarded.
v0.10.0
Added
-
AEC-Bench (
bench/) — an agentic benchmark for Adobe Experience Cloud.
Nobody publishes one, and every MCP server in this space is currently
described by its tool count, a number that measures surface area rather than
competence: fifty tools that 404 score higher than ten that work.Three properties define it. Assertions are made against Adobe, not against
tool output — a task that says "create a segment" is scored by a GET that
finds it, never by the create call's own success flag, because a write
reporting on itself is not evidence. Cleanup is scored: completing a task
while leaving an orphan is not a pass, since a benchmark that dirties the
tenant can only be run once honestly. Tier 1 is read-only and safe against
any tenant including production, because a benchmark nobody dares execute
measures nothing.Tier 1 (5 tasks) and tier 2 (2 tasks) both score 100% against the live
sandbox, with zero residue independently confirmed. Tier 3 (irreversible) is
defined but ships empty on purpose — the tasks that belong there are
non-cancellable and can take 30 days to settle, and a benchmark is not a good
reason to run one.Run with
npm run bench(read-only) ornpm run bench:write. Tier 2+ fails
closed: it refuses unless the expected org and sandbox match the credential
and Adobe classifies the sandbox asdevelopment.Its own first run scored 4/5 — and the failing task was a bug in the task,
not the tool. That distinction is now a comment in the file, because a
benchmark that cannot tell its own bugs from the product's is not one anyone
should trust. -
npm run validate:tools— the tool-level live sweep, promoted to a script. -
Adobe Journey Optimizer tools —
ajo_list_campaignsand
ajo_get_campaign. AJO was already attached to the credential, so its surface
was probed live rather than inferred from documentation, and only what
answered is wrapped: campaigns is the sole reachable AJO surface on this
tenant. Journeys, messages, channel surfaces, content templates, fragments,
offers and decisions all return an HTML 404 — the gateway has no such route —
and are deliberately not implemented. That is the same test the datastream
tools failed for five releases.They use the
ajo_prefix rather thanaep_, because Journey Optimizer is a
separate Adobe product with separate licensing and a name that hides that
makes entitlement failures harder to read.ajo_get_campaigntreats Adobe'sCJMCMP-2044-404("no acceptable version")
as a distinct state rather than a generic 404: the campaign row exists but has
never been published, and flattening the two sends someone hunting for a
campaign that is right there as a draft.Campaign writes are not implemented. The routes exist, but this sandbox
holds no campaign to exercise them against, and shipping an unvalidated write
path into a product that sends messages to real people is not a trade worth
making. -
aep_delete_segment— segments could be created but never deleted, so
every segment an agent made was permanent. That asymmetry left orphans in any
sandbox an agent touched.dryRundefaults true, the confirmation is bound to
the segment id, and deletion is verified by a follow-up GET rather than by
trusting the200that DELETE returns with an empty body. Live-validated
create → delete → verified-gone. Tool count 48 → 49. -
scripts/fixture-run.mjs— creates the minimum throwaway fixtures needed
to exercise read-only tools that require an id, then removes them. It ledgers
each object before creating it, so a crash leaves a record rather than an
orphan. Zero orphans across runs; the sandbox held 0 segments before and after.
Fixed
-
An empty privacy job list was reported as an error. Adobe Privacy Service
answers a query that matched nothing with HTTP 404 and
"detail":"Not able to find job data.".aep_list_privacy_jobssurfaced that
asAEP_404, so "you have no privacy jobs" — the normal state of most
tenants — looked like a broken tool and pushed agents into retrying. It now
returns an empty list. Narrowly scoped: a 404 reading as not authorized or
not provisioned is still an error, because that one needs a human. -
Adobe's nested error detail was being thrown away entirely. The error-body
sanitizer whitelisted top-level keys only, and Adobe nests the useful part
of many errors undererrors:{"errors":{"errorCode":404,"title":"Resource not found","detail":"Not able to find job data."}}errorswas not on the whitelist, so the whole envelope was dropped and
clients received{}. Every error of that shape arrived with no explanation,
which is how a 404 meaning "nothing matched" became indistinguishable from a
404 meaning "no such route". The sanitizer now recurses one level into
errors, applying the same whitelist, so the detail survives while unlisted
fields inside it are still dropped. Onlyerrors, only one level — the
whitelist stays authoritative.
Added
-
scripts/validate-tools-readonly.mjs— live validation that invokes the
registered tool handlers rather than raw HTTP paths, so it reports what an
agent would actually get. It refuses to call any tool not annotated
readOnlyHint: trueand force-clearsAEP_ALLOW_MUTATIONSbefore building
the client, so it cannot mutate. 18 of 18 runnable read-only tools pass; the
other 9 need an id this sandbox cannot supply and are reported as
no-fixturerather than guessed at.It exists because a path-level probe and the tools it was meant to validate
had silently disagreed once before, and neither found the other's bug. Both
fixes above came out of its first run.
v0.9.0
⚠️ Breaking
-
The five datastream tools are removed.
aep_list_datastreams,
aep_get_datastream,aep_create_datastream,aep_update_datastreamand
aep_delete_datastreamare gone. The tool surface is 48, down from 53.They called
/data/core/edge/datastreamsonplatform.adobe.io. That route
does not exist. Probed live on 2026-08-17, every plausible variant returned
an HTML 404 — a gateway-level "no such path", not a JSON authorization error —
so no credential, entitlement, or sandbox could ever have made them work. They
were not blocked; they were pointed at nothing, and had been since v0.3.0.Datastream configuration lives on Reactor (
reactor.adobe.io) as
edge_configurations, behind the Experience Platform Launch API — which
the Developer Console shows as present but disabled for this
organization, and whichreactor.adobe.io/companiesconfirms with
api-key-invalid.Removal rather than deprecation, because the entitlement alone would not have
helped: Reactor is a different host, a different auth scope, a JSON:API
envelope, and company-scoped rather than sandbox-scoped. There was no future
in which the existing code started working. A tool that always fails is worse
than an absent one — it appears intools/list, an agent picks it, and the
failure reads as a confusing gateway error instead of "not available here".No user integration can break, because none can ever have worked.
Full evidence, including the six-path probe table and the HTML-404 vs
JSON-404 distinction that settled it, is in
ADR-0005. ADR-0003, which added
them, is marked superseded.
Changed
-
Tool count 53 → 48; categories 12 → 11.
destructiveHintis now 7
(was 8) andreadOnlyHint27 (was 29). README, hero image, validation
matrix,package.json,server.jsonandCLAUDE.mdall updated — the last
of which had been stale since it still claimed 34 tools across 10 categories. -
scripts/validate-readonly.mjskeeps probing the dead datastream path on
purpose, now flagged as expected-404. It is a regression check: if that
path ever answers in JSON, Adobe has shipped a Platform-side datastream API
and this decision is worth revisiting. -
The
Datastreamtype is deleted rather than left unused. It described a
response shape from an endpoint that never responded; the replacement must be
written from real Reactor responses, not restored from git history.
Added
- A test asserting no registered tool name contains
datastreamand that
the validation matrix documents no Datastreams category — so they cannot
return by accident against the dead path.
v0.8.1
Metadata-only release. No source, dependency, or behaviour changes — cut so the
corrected tool count reaches the npm and MCP Registry listings, which read the
description out of the published tarball and so were still advertising 46.
Fixed
-
The package descriptions said 46 tools; there are 53.
package.jsonand
server.jsonboth carried the stale count, as did the GitHub About text
(corrected separately through the API, since that one is not published from
the repo).These are the three places the tool count is duplicated outside the README,
and none is covered by the test that keeps the README and the registry in
agreement — which is precisely why they drifted while the README stayed
correct. The count last changed at 0.8.0, when the quota tool took the
surface from 52 to 53; the descriptions had been wrong since 0.7.0."46 tools" references under the 0.6.0 and 0.6.2 entries below are left as
written. There were 46 tools then.
Changed
-
The README is rebuilt in the visual style used by
eds-mcp-server — hero image,
mermaid diagrams, column tool tables, collapsible per-client setup. Restyling
it meant re-deriving every figure from the built registry, which turned up
a batch of drift: the comparison table said 46 tools, batch ingestion said 5
(it is 7),destructiveHintsaid 4 and named the wrong set (it is 8),
readOnlyHintsaid 27 (29), and the test count said 57 (402). The
confirmation-gate table listed four gates and was missing five.Two were worse than stale counts. The quickstart told readers to set
AEP_SANDBOX_NAME=prod, in both the.envexample and the Claude Desktop
config — the default 0.7.0 removed precisely because it silently pointed
every request, reads included, at production. AndAEP_ALLOW_MUTATIONSwas
undocumented despite being the switch that gates every write.
v0.8.0
⚠️ Breaking
-
aep_create_record_deleteno longer acceptsALL. The tool's schema
previously told the model: "Pass the literal'ALL'to delete the identities
from every dataset in the sandbox." Anyone whose prompt led a model to take
that suggestion in a shared sandbox would have issued a non-cancellable
deletion across datasets belonging to other people.ALL— along with*,
all,any,prod,production, comma-separated lists, and blank or
malformed IDs — is now refused before any network call. The tool requires the
exact ID of one dataset. -
The record-delete confirmation phrase changed from a generic
I understand this is irreversibleto
DELETE RECORDS <datasetId> <identityDigest>, where the digest is a
SHA-256 over the canonical, order-independent namespace/identity set. The old
phrase named neither the dataset nor the identities, so a confirmation
granted for one deletion authorised any other. Existing callers passing the
old phrase will now receiveCONFIRMATION_REQUIRED.
Added
-
aep_get_data_lifecycle_quota— read-onlyGET /data/core/hygiene/quota,
with an optionalquotaTypefilter. Reports dataset-expiration and
consumer-delete-identity quota consumption. Live-validated. -
Record-delete preflight. Before submitting, the tool resolves the dataset
and its schema and refuses when the dataset has no primary identity or
identityMap(NO_PRIMARY_IDENTITY), or when it already has an active
expiration scheduled (DATASET_HAS_ACTIVE_EXPIRATION). Adobe requires the
former; the latter is a conflict worth stopping on rather than racing. -
dryRunonaep_create_record_delete, defaulting to true, making zero
network calls and returning the request that would be sent.
Security
-
Tenant identifiers removed from the tracked tree. The repository is
public and carried two real IMS org IDs, a development sandbox name, an
Adobe Developer Console project name, and Adobe support case numbers, plus
twelve run ledgers and four pinned cleanup scripts naming live dataset,
batch and TTL IDs. None of these are credentials — no credential was ever
committed, across all 56 commits — but they are tenant metadata and did not
belong in public. Documentation now uses<IMS_ORG_ID>,
<DEVELOPMENT_SANDBOX>,<DATASET_ID>,<BATCH_ID>and<TTL_ID>
placeholders; ledgers and cleanup scripts are untracked and gitignored while
still being written locally. -
The wrong-org guard is now supplied by the caller and fails closed.
scripts/validate-readonly.mjsandscripts/phase-runner.mjsrequired
AEP_EXPECTED_ORG_ID/AEP_EXPECTED_SANDBOX_NAME; a missing expectation is
fatal, because "nobody said which tenant" must never read as "any tenant".
A mismatch is now fatal too — it previously printedUNEXPECTED (...)and
then issued the requests anyway, and a warning nobody reads is not a guard.
Neither the expected nor the actual value is ever printed: the harness shows
a 12-character SHA-256 fingerprint of each, which is enough to see whether
two values agree without putting a tenant ID into a CI log. -
Identity values are never echoed, logged, or returned by
aep_create_record_delete— not in dry-run output, not in error messages,
not in logs. Output carries only an identity count, the namespace names, and
the 12-character digest. A record-delete request is by nature a list of real
people's email addresses and device IDs; a tool that reflects them back
copies them into every transcript and log sink that touches it.
Changed
-
Tool count is now 53. Enumerated from the registry, not counted by hand: a
new test registers every tool and asserts thatdocs/VALIDATION-MATRIX.md
names exactly that set, so the document cannot drift from the code again. -
Corrected: there are five datastream tools, not four, and there is no
cancel-batch tool. Both errors came from counting rows in a table rather than
tools in the registry — the matrix had collapsedaep_update_datastreamand
aep_delete_datastreamonto one row, and had listed a cancel-batch tool that
has never existed. Batch Ingestion is 7 tools. -
docs/VALIDATION-MATRIX.mdrewritten. It had opened with "No tool in this
table has been exercised against a live Adobe tenant" since 2026-08-11 — true
when written, false from 2026-08-14. It also documented
aep_create_dataset_expirationasPUT /ttl/{id}with adryRunmode Adobe
does not offer. It now records, per tool, what was actually executed live.
Notes
-
aep_create_record_deletehas never been executed against a live tenant,
and deliberately so. Work-order creation is on the permanent NEVER list for
the shared development sandbox: the operation is asynchronous, non-cancellable, and
may take up to 30 days. Its contract is verified against Adobe's
documentation and covered by 23 unit tests. Both consumer-delete quota
counters reading zero independently confirms none has ever been issued. -
aep_get_work_order_statusremains unvalidated. It needs an existing
workorderId, and no work order exists in this sandbox — creating one purely
to obtain an ID is not a justification for an irreversible operation.
v0.7.0
⚠️ Breaking
-
Mutations are now off by default. No write, update, or delete tool can
execute unlessAEP_ALLOW_MUTATIONS=trueis set. If you were running 0.6.x
with writes working, they will be refused after upgrading until you add that
variable.This is deliberately separate from
AEP_MODE. Choosing a write mode says
"here is how much I want this server to trust the sandbox"; it should not
also say "yes, this server may change my data". One variable doing both jobs
meant a single setting could open everything. -
A sandbox named
prodorproductionis refused for all mutations,
unconditionally. This check runs before write-mode resolution, so
AEP_MODE=productionno longer lifts it. Override with
AEP_I_UNDERSTAND_THIS_WRITES_TO_PROD=trueonly if your sandbox is genuinely
namedprodand you intend to write to it.This is not the server inferring safety from a name, which it still never
does. The inference is asymmetric: trusting a name to allow a write is
unsafe, because a production sandbox can be called anything. Trusting a name
to deny one is safe — the worst case is a refusal you can override
deliberately.
Fixed
-
aep_create_record_deletesent the wrong payload and would have failed
against any live tenant. It posted a flatidentities: [{namespace, id}]
array. Adobe's Data Lifecycle API requiresnamespacesIdentities— an array
of{ namespace: { code }, ids: [...] }grouped by namespace.The tool schema still accepts the flat form, which is markedly easier to
produce correctly, and converts on the way out. Found by auditing against
Adobe's documentation, not by the test suite, which was green throughout. -
AEP_MODE=productionbypassed the sandbox check entirely.
assertWriteAllowedreturned early on production mode before evaluating the
sandbox, so one environment variable was enough to permit a mutation against
a sandbox namedprod. Both new gates now run ahead of mode resolution. -
Raw Adobe error response bodies were logged at debug level. Adobe echoes
request context, and on Profile and Identity surfaces that can include
identity values — the PII the redact list exists to exclude. Redaction cannot
reach inside an opaque string, and debug level is not protection because
operators raise log levels during incidents. Now behind
AEP_LOG_RESPONSE_BODIES, default off. -
A source comment claimed Adobe's single-
PUTceiling for batch upload was
512 MB. The documented figure is 256 MB. The tool's own 100 MB cap was always
safely below both, so no behaviour was wrong — but a wrong constant in a
comment becomes a wrong constant in code the first time someone raises a
limit and trusts it.
Added
-
scripts/validate-readonly.mjs— aGET-only live validation harness,
hard-asserted against non-GET methods and safe to run against production. It
probes schemas, datasets, batches, hygiene work orders, dataset expirations,
segment definitions, datastreams, privacy requests, and Sandbox Management,
and reports explicitly when the write guard will be unable to resolve a
sandbox type — in which casesafemode fails closed on every mutation, by
design. Refuses to start unlessAEP_ORG_IDends in@AdobeOrg. -
docs/VALIDATION-MATRIX.md— per-tool status across documentation-verified,
mocked, live-read, and live-write for the 12 Batch Ingestion and Data
Lifecycle tools. None are live-validated, and the document says so
plainly rather than leaving it to be discovered. -
Adobe's documented 100,000-identity ceiling is now enforced on
aep_create_record_delete. -
55 new tests (148 → 176): 31 covering the mutation gates and every bypass
route found, 9 on the record-delete wire format, and 17 on log redaction —
which deliberately assert the gap as well as the coverage, since pino's
*.wildcard matches one level and a secret nested deeper passes through. -
AEP_ALLOW_MUTATIONS,AEP_I_UNDERSTAND_THIS_WRITES_TO_PROD, and
AEP_LOG_RESPONSE_BODIESdocumented in.env.example.
Changed
- The
.env.examplesample sandbox is no longerprod— shipping an example
naming the one sandbox the server refuses to mutate was needlessly confusing.
v0.6.3
Fixed
- Shell injection in the release workflow. The GitHub Release step
interpolated the changelog into a shell command, so backticks in release
notes were executed by bash as command substitution. This broke the 0.6.2
release after npm had already published, and meant anything landing a
CHANGELOG entry could run arbitrary commands in CI. Notes are now written to
a file and passed with--notes-file; no step output is interpolated into a
shell anywhere in the workflow.
v0.6.2
Fixed
-
The server exited instead of starting when Adobe credentials were invalid.
A failed IMS token fetch at startup was fatal, so an MCP client could never
complete a handshake or calltools/listwithout working credentials — the
process was gone before it spoke any protocol. A user could not inspect the
tool surface before configuring auth, and registries that verify a server
with placeholder credentials saw only a crash, which is why this server was
marked "cannot be installed" and "quality — not tested" on Glama.Authentication failure now logs a prominent warning and the server continues.
Individual calls already report auth problems as structuredAEP_AUTH_*
errors, which is a clearer place for them to surface. Nothing is weakened:
without a token every request fails at the Adobe boundary, and sandbox
resolution fails closed, so writes stay blocked.
Added
- End-to-end MCP protocol test (
tests/integration/handshake.test.ts) that
runs the built server with deliberately invalid credentials and asserts
initializecompletes,tools/listreturns all 46 tools, annotations
survive the wire, and a tool call fails as a structured error rather than a
crash. Skips cleanly whendist/is absent. Suite: 112 → 117.
Changed
- Dockerfile: Node 20 → 22; runs as the unprivileged
nodeuser; and the
build stage now fails if the compiled output cannot load and register tools,
so a broken image cannot be published.
v0.6.1
First release published by the automated pipeline rather than by hand.
Added
- Automated release pipeline. Pushing a tag runs gates (tag matches
and , version not already on npm, CHANGELOG entry
present, typecheck, test, build, and a smoke check that the built entrypoint
actually registers its tools), then publishes to npm, creates the GitHub
Release from the CHANGELOG, and registers with the Official MCP Registry. - npm Trusted Publishing via OIDC. There is no npm token stored anywhere;
GitHub mints a short-lived credential per run and npm generates the
provenance attestation itself. - for the Official MCP Registry, published as
**** — a domain-verified namespace proved by a TXT record
on , so it belongs to the company rather than to an individual
GitHub account. - recording listing maintainers.
- and , plus a
lifecycle hook so {
'@focusgts/aep-mcp-server': '0.6.1',
npm: '12.0.2',
node: '22.23.1',
acorn: '8.16.0',
ada: '2.9.2',
amaro: '1.1.8',
ares: '1.34.6',
brotli: '1.1.0',
cjs_module_lexer: '2.2.0',
cldr: '48.0',
icu: '78.2',
llhttp: '9.4.2',
modules: '127',
napi: '10',
nbytes: '0.1.3',
ncrypto: '0.0.1',
nghttp2: '1.69.0',
openssl: '3.5.7',
simdjson: '4.5.0',
simdutf: '6.4.2',
sqlite: '3.51.3',
tz: '2026a',
undici: '6.27.0',
unicode: '17.0',
uv: '1.51.0',
uvwasi: '0.0.23',
v8: '12.4.254.21-node.56',
zlib: '1.3.1-e00f703',
zstd: '1.5.7'
} keeps in step
automatically. - covering the one-time setup, the release procedure, and
recovery when a release fails before or after the publish step.
Note for npm users
This is the first npm release since 0.3.1. Everything between — the pagination
and sandbox-scoping fixes, batch ingestion, data lifecycle, the write modes and
production-write guard, and tool annotations — arrives with this version. See
the 0.4.0, 0.5.0, and 0.6.0 entries below.