Command Palette

Search for a command to run...

UnylyUnyly
Browse all

juan-sibbo/gam-seller-mcp-node

FreeMaintained

Governed, GDPR-first sell-side MCP server: exposes Google Ad Manager inventory to buyer AI agents with read-only product discovery, firm pricing, and a revocabl

GitHubEmbed

About

Governed, GDPR-first sell-side MCP server: exposes Google Ad Manager inventory to buyer AI agents with read-only product discovery, firm pricing, and a revocable commitment primitive — no ad-server writes, no sensitive data, every decision audited. npx gam-seller-mcp-node.

README

npm version npm downloads CI License: MIT TypeScript MCP

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.


What problem does this solve?

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

How it works

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:

Buyer agent
    │
    ├── well_known_capabilities   ← Signed trust anchor. Check this first.
    │       Returns: RS256-signed capability document, node identity, privacy posture.
    │
    ├── discover_products         ← What can I buy here, and at what firm price?
    │       Returns: product families the buyer is entitled to see (e.g. "Pre-Roll Video"),
    │               each with its firm list price when the publisher has configured one.
    │       Never returns: deal IDs, internal IDs, raw inventory, exact per-impression pricing.
    │
    ├── get_forecast              ← How available is this family next quarter?
    │       Returns: Low / Mid / High availability bucket.
    │       Never returns: exact impression counts, CPM curves, floor prices.
    │
    ├── create_intent             ← Commit to a product at its current firm price (with TTL).
    │       Records a firm, time-boxed buying intent — rejected if the price is stale or
    │       mismatched. NOT a GAM order and NOT an inventory hold; it is the handoff artifact
    │       the classic sales rails pick up. Buyer-scoped: you can only ever commit as yourself.
    │
    └── revoke_intent             ← Withdraw one of your own active intents by id.

Every call flows through the same pipeline before any domain logic runs:

  Buyer request
       │
       ▼
  [SEC-GATE-3]  Replay detection — deduplicate client_request_id
       │
       ▼
  [Auth]        RS256 token validation → identity confirmed or AUTH_FAILED
       │
       ▼
  [Policy]      Surface denylist → entitlement check → scope check (Default-Deny)
       │
       ▼
  [Rate limit]  N=1 / T=30s per buyer_id
       │
       ▼
  [Domain]      Catalog / ForecastEngine — synthetic today, real GAM adapter in progress
       │
       ▼
  [Audit]       Append-only hash-chained ledger, buyer pseudonymized (HMAC)
       │
       ▼
  Response to buyer

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.

Quick start

Run a full pilot in one command. scripts/pilot.sh brings 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) — see docs/PILOT-QUICKSTART.md. The reference buyer agent lives at examples/buyer-client-ts/agent.ts; hosting behind TLS is a filled-in-the-blanks recipe in deploy/.

Install in an MCP client (via npx)

Add the server to your MCP client (Claude Desktop, Claude Code, Cursor, …):

{
  "mcpServers": {
    "gam-seller": {
      "command": "npx",
      "args": ["-y", "gam-seller-mcp-node"]
    }
  }
}

Or run it directly (stdio transport — the default for MCP clients):

npx -y gam-seller-mcp-node

Demo mode. With no config of your own, the node boots on a bundled pilot-publisher example (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 the token argument to discover_products / get_forecast to see the example families, prices and forecasts.

For a real deployment, point MCP_CONFIG_DIR at a directory holding your own deployment.json, catalog.json, entitlements.json and pricing.json:

MCP_CONFIG_DIR=/etc/gam-seller/config npx -y gam-seller-mcp-node

From source

git clone https://github.com/juan-sibbo/gam-seller-mcp-node.git
cd gam-seller-mcp-node
npm install
npm run build
npm run start:http   # HTTP transport on 127.0.0.1:3900

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:

npm run demo          # or: npx tsx demo/run-demo.ts

With Docker

docker compose up

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.

Configure for your publisher

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):

deployment.json     # DSR contact, controller model, data retention window
catalog.json        # product families + per-buyer access grants
entitlements.json   # which buyers are entitled to which MCP surfaces
pricing.json        # firm list prices per family (fail-closed on expiry)
forecast.json       # OPTIONAL — seed availability buckets from real numbers (still synthetic-labeled)

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:

  • Seed the forecast with the publisher's own availability, exported once from a GAM report, via an optional 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.
  • Close the handoff loop so a committed intent reaches the publisher's sales rails, via 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.
  • Harden for the road: 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).

Why not just use the GAM API directly?

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

Current status

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

Architecture

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

Security model

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.

Regulatory posture

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.

Machine-readable trust anchor

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):

  • Whether the catalog and forecast data are synthetic or live (data_source)
  • Whether head-hash anchoring uses a local file or cloud Object Lock (anchor_store)
  • Whether the node is in demo mode or serving a real publisher config (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.

Testing

npm test                              # full suite (vitest)
python3 sandbox/buyer-agent-probe.py  # external Python interop probe (no shared code with server)

The test suite includes:

  • Unit tests for each module (policy, pricing, identity, audit, catalog, forecast, DSR)
  • Integration tests over real in-memory MCP transports (tests/server.test.ts)
  • HTTP transport tests over a real ephemeral-port HTTP server (tests/http.test.ts)
  • End-to-end session tests simulating a full buyer-agent session (tests/buyer-agent-session.test.ts)
  • External Python probe that exercises the HTTP transport without any shared Node.js code

CI runs on every push via GitHub Actions.

Data protection

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.

Roadmap

See the open issues for the full roadmap. Highlights:

  • Real GAM adapter — wire getAvailabilityForecast via the ForecastService SOAP API
  • Buyer agent SDKs — Python and TypeScript client libraries for the MCP buyer flow
  • OpenRTB 3.0 taxonomy — align family_id scheme with IAB standards
  • Well-known observability properties — expose data_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.)

Contributing

See CONTRIBUTING.md. Issues tagged good first issue are a good starting point.

License

MIT — see LICENSE.

from github.com/juan-sibbo/gam-seller-mcp-node

Installing juan-sibbo/gam-seller-mcp-node

This server has no published package — it is built from source. Open the repository and follow its README.

▸ github.com/juan-sibbo/gam-seller-mcp-node

FAQ

Is juan-sibbo/gam-seller-mcp-node MCP free?

Yes, juan-sibbo/gam-seller-mcp-node MCP is free — one-click install via Unyly at no cost.

Does juan-sibbo/gam-seller-mcp-node need an API key?

No, juan-sibbo/gam-seller-mcp-node runs without API keys or environment variables.

Is juan-sibbo/gam-seller-mcp-node hosted or self-hosted?

Self-hosted: the server runs locally on your machine via the install command above.

How do I install juan-sibbo/gam-seller-mcp-node in Claude Desktop, Claude Code or Cursor?

Open juan-sibbo/gam-seller-mcp-node on unyly.org, pick your client tab (Claude Desktop, Claude Code, Cursor) and press Install — the config is generated automatically, no JSON editing.

Related MCPs

Compare juan-sibbo/gam-seller-mcp-node with

Not sure what to pick?

Find your stack in 60 seconds

Author?

Embed badge for your README

Browse similar

All ai MCPs