The full upstream README, mirrored here for reference. Install config, tool schemas, adoption signals, and an original overview live on the Gam Seller MCP Node listing page.
A Model Context Protocol server that exposes sell-side ad inventory to buyer-side AI agents: discovery, firm pricing, and a buyer-scoped soft commitment primitive. No writes to an ad server exist. The Google Ad Manager adapter is not yet connected; catalog and forecast data are synthetic.
Sell-side ad inventory (availability, pricing, product structure) lives inside ad servers that hold commercially sensitive and sometimes personal data. Giving an AI buyer agent direct API access to GAM or a similar system creates three risks:
| Risk | Without this project | With this project |
|---|---|---|
| Data over-exposure | Agent can read raw avails, deal IDs, exact floor prices | Only coarse buckets and pre-declared families |
| Accidental writes | Agent SDK can create orders, modify line items | No ad-server writes exist; the only write is a buyer's own soft commitment, which can never become a GAM order or an inventory hold |
| No accountability | API calls are logged but not auditable | Hash-chained audit ledger; every allow/deny recorded |
A buyer agent connects via MCP and gets five tools — three read-only, plus a buyer-scoped commitment primitive (create/revoke) that is the sole write surface:
Every call flows through the same pipeline before any domain logic runs:
Each request-path gate rejects on failure. One honest caveat to the diagram above:
client_request_id (the replay-guard deduplication key) is optional by default; a request
that omits it bypasses SEC-GATE-3. A deployment can set MCP_REQUIRE_IDEMPOTENCY_KEY to make it
mandatory on every authenticated surface (fail-closed) — off by default for back-compat.The rate-limit stage covers every authenticated tool — the read surfaces, create_intent,
and revoke_intent — so no authenticated surface bypasses it.
A corrupted or tampered on-disk ledger is detected on startup and the node refuses to serve (fail-closed on load, plus a chain-integrity verify before the first request) rather than resetting to an empty chain.
create_intent runs the same gates and adds one more before it records anything: the buyer's
price_ref must match the family's current firm price, or the request is rejected.
Run a full pilot in one command.
scripts/pilot.shbrings the node up on your config with production guards on, mints a buyer token per entitled buyer, and prints how to drive a buyer agent through the whole loop (discover → forecast → commit → revoke) — seedocs/PILOT-QUICKSTART.md. The reference buyer agent lives atexamples/buyer-client-ts/agent.ts; hosting behind TLS is a filled-in-the-blanks recipe indeploy/.
Add the server to your MCP client (Claude Desktop, Claude Code, Cursor, …):
Or run it directly (stdio transport — the default for MCP clients):
Demo mode. With no config of your own, the node boots on a bundled
pilot-publisherexample (illustrative catalog, prices and forecasts) and says so on stderr — it starts instead of failing, so you can try the tools immediately. Because buyer surfaces always require a token (there is no anonymous path, even in demo), the node prints a ready-to-use demo buyer token on startup: copy it and pass it as thetokenargument todiscover_products/get_forecastto see the example families, prices and forecasts.For a real deployment, point
MCP_CONFIG_DIRat a directory holding your owndeployment.json,catalog.json,entitlements.jsonandpricing.json:
Run the full buyer-agent walkthrough (scripted demo) — the five native tools driven over a real in-process MCP transport, ending in the governed refusals (fail-closed auth, Default-Deny, fail-closed pricing) and a verified audit chain:
The node starts on 127.0.0.1:3900. The well-known document is at
/.well-known/seller-mcp-capabilities. Persistent volumes for keys and audit data are
pre-configured in docker-compose.yml.
Four JSON files drive all publisher-specific behaviour — no code changes needed. Place them
in config/ (from-source) or in the directory named by MCP_CONFIG_DIR (npx/containerised):
Invalid config always fails closed: a malformed file stops the node rather than running
with a silently different access policy. Absent config (no config/ and no MCP_CONFIG_DIR)
drops to the bundled config/examples/pilot-publisher/
example — demo mode, announced on stderr — so the node is never a broken install, only ever a
real deployment or a clearly-labelled demo.
Taking a pilot onto real inventory (short of a live GAM connection) is all configuration —
see docs/PUBLISHER-DEPLOYMENT.md:
forecast.json (template: config/examples/pilot-publisher/forecast.sample.json).
Buckets become realistic while every result stays synthetic: true — pre-loaded is not a live
read, so no live-GAM claim is made.MCP_INTENT_HANDOFF=file (a local JSONL drop an operator forwarder tails). The node makes no
outbound calls (SSRF/egress deny-all) — a handoff record is a notification, never a GAM order
or inventory hold.MCP_REQUIRE_OPERATOR_CONFIG=1 (refuse to boot on demo config),
MCP_REQUIRE_IDEMPOTENCY_KEY=1 (close the replay-bypass), MCP_ANCHOR_SINK=tsa (anchor the
audit trail to a third party).| Approach | Data exposure | Writability | Auditability | AI-agent friendly |
|---|---|---|---|---|
| Raw GAM API | Everything in the account | Full CRUD | Logging only | Poor (SOAP/REST, no MCP) |
| OpenRTB bid requests | User-level data, floor prices | Bid-only | None | Poor |
| This server | Coarse families + bucket forecasts | Buyer's own soft commitment only (no GAM writes) | Hash-chained ledger | Native MCP |
Working prototype. The full request pipeline (auth → policy → rate-limit → domain → audit),
the buyer-scoped commitment primitive (create_intent / revoke_intent, with TTL expiry),
the audit ledger, GDPR data-subject-rights toolkit, Docker packaging, HTTP transport,
and a live interop probe (Python buyer agent simulation) are all implemented and tested.
The persistence layer is hardened for restarts (append-only, atomic writes, durable rotation
state, fail-closed load), and the head-hash anchor is append-only with selectable external WORM
backends (RFC 3161 timestamping / S3 Object Lock) — see Known limitations
for the residual (a live write-once destination is an operator infra act).
Not yet wired: a live Google Ad Manager connection. The catalog and forecast data are
synthetic, loaded from local config. The GAM ForecastService SOAP adapter interface exists
(src/forecast/source.ts) as a stub — it throws on any call until a
service account is provisioned (DP-AB-01 §5.2). See the
open issues for the roadmap.
Known limitations — dated status. Closed rows are kept on purpose: a limitations list that changes state over time is both a proof of honesty and a proof of progress.
| Limitation | Anchor | Status | Closed by |
|---|---|---|---|
Attribution (buyer_id / request_id) is stored per entry but sits outside the chain's tamper-evidence hash | audit/event.ts | Design decision, not a defect — traceability vs. erasability (ADR-4) | — |
Head-hash anchor rewrote its whole file each write (writeFileSync) — not append-only, no external WORM | audit/anchor.ts | ✅ Closed 2026-08-23 — append-only JSONL + injectable AnchorSink; selectable tsa (RFC 3161) and s3 (S3 Object Lock) backends via MCP_ANCHOR_SINK | #92 #94 #95 |
| External WORM anchoring needs the operator to point at a live write-once destination (a TSA URL, or a locked bucket) — the node ships the backends, not the destination | audit/anchor-tsa.ts, audit/anchor-s3.ts | Open — deployment boundary (infra act) | — |
client_request_id (replay guard) is optional by default; omitting it bypasses SEC-GATE-3 | src/server.ts | Mitigated 2026-08-24 — MCP_REQUIRE_IDEMPOTENCY_KEY makes it mandatory on every authenticated surface (fail-closed); optional by default for back-compat | #82 |
| No TLS in transit (a reverse proxy is expected to terminate) | — | Open — deployment boundary | — |
revoke_intent is not covered by the rate-limit stage | src/server.ts | ✅ Closed 2026-08-18 — now behind the rate-limit gate like every authenticated surface | #80 |
GDPR DSR CLI (scripts/dsr.ts, …) not shipped in the npm package | package.json files | ✅ Closed 2026-08 — ships as the gam-seller-dsr bin | #78 |
| Ledger loaded fail-open — a corrupt file reset to an empty chain | audit/ledger.ts | ✅ Closed 2026-08-07 | #65 |
| Chain integrity not verified before serving on startup | src/server.ts | ✅ Closed 2026-08-07 | #65 |
See docs/ARCHITECTURE.md for the module map and data-flow diagrams.
Key modules:
| Module | Role |
|---|---|
src/server.ts | MCP tool definitions + request pipeline |
src/policy/ | Default-Deny engine, entitlement store, surface allowlist/denylist |
src/identity/ | RS256 key management, token issuance/validation, revocation denylist |
src/audit/ | Hash-chained ledger, HMAC pseudonymization, append-only head-hash anchoring with selectable WORM backends (anchor-tsa.ts, anchor-s3.ts) |
src/pricing/ | Firm list price store, expiry-aware (fail-closed on stale prices) |
src/forecast/ | Bucket engine + GAM adapter seam (synthetic today) |
src/dsr/ | GDPR Art. 15/17/18/20 data-subject-rights toolkit (also shipped as the gam-seller-dsr bin) |
src/catalog/ | Product family store, per-buyer access grants |
Default-Deny. Every request is denied unless an explicit entitlement says otherwise — there is no "allow by default" path in the code.
Structural allow/denylist (SEC-GATE-*). Response surfaces are governed by a fixed list enforced
at the policy layer, independent of which tool was called. Exact pricing, deal IDs, raw availability
numbers, cross-buyer state, real inventory holds (soft-lock), and any ad-server write are permanently
denied. The one permitted write is a buyer's own commitment (create_intent / revoke_intent),
which required an explicit amendment to the surface allowlist and stays buyer-scoped. Adding a new
tool in the future cannot bypass this.
Opaque errors. A denied request, a failed authentication, and a revoked token all return the
same generic AUTH_FAILED code. Internal reasons never reach the buyer.
Audit-first. Every allow/deny is written to the ledger before the response is sent.
Buyer buyer_id values are pseudonymized (HMAC-SHA256) before entering the chain. Note that
buyer_id and request_id, while stored in each audit entry, are not included in the
hash-chain's canonical input (audit/event.ts:50); those fields are not covered by the
chain's tamper-evidence guarantee.
Privacy by construction. Responses carry only inventory-level data (product family, coarse bucket). User-level attributes don't exist in any response path.
See docs/DESIGN-PRINCIPLES.md for the full reasoning.
The AEPD (Spain's data protection authority) published guidelines on agentic AI systems in February 2026. The four recommendations most relevant to an ad-inventory node map directly to existing design decisions:
| AEPD recommendation | This node |
|---|---|
| Protection by design and by default | Default-Deny: every surface denied unless an explicit entitlement grants access |
| Record and document agent actions | Append-only hash-chained audit ledger; every allow/deny recorded before the response is sent |
| Control what leaves toward third parties, and with what traceability | Structural egress allowlist (SEC-GATE-*); exact pricing, deal IDs and raw availability permanently blocked |
| Govern agent memory with purpose and retention rules | DSR toolkit (Arts. 15/17/18/20); configurable retention window enforced on the audit ledger |
This alignment is declared machine-readably in the signed well-known document
(/.well-known/seller-mcp-capabilities) under privacy_posture.regulatory_alignment_declared:
["GDPR", "AEPD-orientaciones-IA-agentica-2026"]. A buyer agent or auditor can verify it
cryptographically without trusting this README.
The node does not make legal determinations — whether a given processing has a legitimate basis, whether consent is valid, whether a particular treatment is permitted. Those judgements belong to the controller (the broadcaster). The node provides the mechanisms; the controller applies the criteria. This boundary is what keeps the node's design stable regardless of how the EU Data Act negotiations resolve.
The /.well-known/seller-mcp-capabilities endpoint returns an RS256-signed JWT. A buyer agent
reads and verifies this document before the first authenticated request. The privacy_posture
block inside it is machine-readable and cryptographically bound to the node's keypair:
| Property | Current value | Meaning |
|---|---|---|
end_user_personal_data | "none" | No end-user personal data in any response path |
audience_segmentation | "not_offered_v1" | No audience targeting surfaces |
tc_string_consumption | "none" | Node does not consume TC strings (server-to-server, PATH A) |
device_storage_access | "none" | No device storage access (ePrivacy N/A) |
jurisdiction | ["ES", "EU"] | Declared operating jurisdiction |
regulatory_alignment_declared | ["GDPR", "AEPD-orientaciones-IA-agentica-2026"] | Declared alignment |
dsr_contact | from deployment.json | Contact for data-subject requests |
controller_model | from deployment.json | Publisher's declared controller role |
audit_retention | from deployment.json | Hot/archive retention windows in days/months |
Not yet in the well-known document (properties that remain implicit):
data_source)anchor_store)deployment_mode)These properties would allow a buyer agent to programmatically distinguish a demo deployment from a production one, and a locally-anchored node from one with external tamper-evidence. They are not present in the current version.
The test suite includes:
tests/server.test.ts)tests/http.test.ts)tests/buyer-agent-session.test.ts)CI runs on every push via GitHub Actions.
Raw buyer_id values never enter the audit ledger — only an HMAC pseudonym. The
src/dsr/toolkit.ts implements export, restriction, and erasure of a
buyer's audit data (GDPR Art. 15/17/18/20). The node stores nothing about end users; the DSR
scope is exactly what it records — B2B buyer organization pseudonyms and their request events.
Distribution note. The DSR toolkit ships in the npm package as the gam-seller-dsr bin, so
export / restriction / erasure can be run without a checkout. The token-management scripts
(scripts/issue-buyer-token.ts, scripts/revoke-token.ts) remain source-only — publishers who
need them must clone the repository.
See the open issues for the full roadmap. Highlights:
getAvailabilityForecast via the ForecastService SOAP APIfamily_id scheme with IAB standardsdata_source / anchor_store / deployment_mode so a buyer agent can distinguish demo from production programmatically(The Prometheus /metrics endpoint is already shipped — loopback-only, opt-in.)
See CONTRIBUTING.md. Issues tagged
good first issue
are a good starting point.
MIT — see LICENSE.