Gam Seller Mcp Node

io.github.juan-sibbo由 GitHub 帳號 juan-sibbo 發布v0.11.1更新於 Oct 24, 2026

Governed MCP seller node: buyer agents discover inventory and register intents; no ad-server writes

概覽

AI 產生的概覽

一個受治理的賣方廣告庫存 MCP 節點,讓買方 AI 代理探索產品系列、查詢可用性區間,並記錄買方範圍內的軟性購買意向。

可以做什麼
透過 stdio 提供五個工具:well_known_capabilities 回傳簽章信任錨點;discover_products 列出已授權的產品系列與固定標價;get_forecast 回傳低/中/高可用性區間;check_availability 判斷某期間能否滿足要求的曝光量;create_intent/revoke_intent 記錄或撤回買方範圍內、有期限的承諾。每個請求都會經過重播偵測、RS256 權杖驗證、預設拒絕政策、速率限制、揭露結構描述閘門,以及僅可附加的雜湊鏈稽核帳本。不會對廣告伺服器寫入;唯一的寫入是買方自己的軟性承諾。
適用情境
當發布方希望讓買方 AI 代理查看賣方庫存可用性與固定價格,同時不暴露原始可用量、交易 ID 或精確底價,也不授予廣告伺服器寫入權限時使用。適合需要可治理、可稽核交接產物的試點部署,而非直接存取 GAM API。
執行需求
以 npm 套件形式在本機執行(npx gam-seller-mcp-node),使用 stdio 傳輸,需要 Node.js。選用環境變數 MCP_CONFIG_DIR 指向包含 deployment.json、catalog.json、entitlements.json 與 pricing.json 的目錄;未設定時節點會執行內建示範並在啟動時印出示範買方權杖。買方介面一律需要 RS256 權杖。選用的即時 GAM 預測需要 gam.json 以及具備讀取角色的 GAM 服務帳號;選用稽核錨定使用 MCP_ANCHOR_SINK。
安裝前請注意
買方介面需要權杖,即使在示範模式下也沒有匿名路徑;節點會在啟動時印出可直接使用的示範買方權杖。create_intent 依目前標價記錄有期限的確定購買意向,是唯一的寫入介面,但它不是 GAM 訂單也不是庫存保留。選用的即時 GAM 預測會在啟動時及每 30 分鐘向 Google OAuth 端點與 Ad Manager SOAP API 發出外部呼叫;選用稽核錨定可將帳本頭雜湊傳送到設定的 TSA 或 S3 Object Lock 儲存桶。傳輸層沒有 TLS,需由反向代理終止。設定無效時會故障關閉。

安裝

在 SourceWeft 中

  1. 開啟 儀表板中的 Gam Seller Mcp Node,將其新增到工作區。
  2. 為需要使用其工具的對話啟用該服務。

Desktop only,透過 STDIO。 STDIO 服務會啟動本機處理程序,因此需要 SourceWeft 桌面主機。

其他 MCP 客戶端

參照 儲存庫 中的啟動說明。

README

Governed MCP seller control-plane prototype for future GAM integration.

[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. Forecasts can come live from Google Ad Manager (opt-in, read-only ForecastService snapshot); the catalog is operator config, and without a GAM connection forecast data is 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:

RiskWithout this projectWith this project
Data over-exposureAgent can read raw avails, deal IDs, exact floor pricesOnly coarse buckets and pre-declared families
Accidental writesAgent SDK can create orders, modify line itemsNo 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 accountabilityAPI calls are logged but not auditableHash-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 formats, channel and sites, and its firm list price    │               when the publisher has configured them.    │       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.    │    ├── check_availability        ← Can you deliver 2.8M impressions of this family in October?    │       Returns: available / partial (up to ~2.7M, ~1.3M viewable) / unavailable, as of the    │       last forecast — and, when it does not fit, where it would ("November: 3.1M").    │       Volumes come from the forecast rounded down to 2 significant figures    │       (configurable by the publisher).    │       Never returns: other buyers' bookings, floor prices, audience data.    │    ├── 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, seeded or live GAM forecast snapshot       │       ▼  [Disclosure]  Response checked against the tool's strict schema — undeclared field → withheld       │       ▼  [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 required by default on every authenticated surface since v0.9.0 — a request without it is rejected, so SEC-GATE-3 cannot be bypassed by omission. An operator can explicitly opt out for legacy clients with MCP_REQUIRE_IDEMPOTENCY_KEY=0, which reopens that bypass.

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

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

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

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

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

From source

bash
git clone https://github.com/juan-sibbo/gam-seller-mcp-node.gitcd gam-seller-mcp-nodenpm installnpm run buildnpm 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:

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

With Docker

bash
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 windowcatalog.json        # product families + per-buyer access grantsentitlements.json   # which buyers are entitled to which MCP surfacespricing.json        # firm list prices per family (fail-closed on expiry)forecast.json       # OPTIONAL — seed availability buckets from real numbers (still synthetic-labeled)gam.json            # OPTIONAL — live GAM forecast (network, service-account key path, family → targeting)

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.
  • Connect GAM live with an optional gam.json (template: config/examples/pilot-publisher/gam.sample.json) and a service account added to the GAM network with a read role that can run forecasts. The node asks ForecastService.getAvailabilityForecast for every configured family × period at boot and every 30 minutes, using prospective line items that are never saved — nothing in GAM is created, modified or reserved. Buyers are answered from that snapshot as Low/Mid/High buckets with synthetic: false; raw availability never leaves the node. Precedence: gam.json > forecast.json > synthetic.
  • 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 handoff makes no outbound call — forwarding is the operator's process, and a URL value is refused. 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_ANCHOR_SINK=tsa (anchor the audit trail to a third party).

Network egress — declared and bounded, not deny-all. Buyer request handling makes no outbound network calls. The live GAM forecast (gam.json) calls Google's OAuth token endpoint and the Ad Manager SOAP API for the configured network, at boot and on the 30-minute refresh cycle — never inside a buyer request, so buyers cannot generate load on the publisher's GAM. Optional audit anchoring can generate operator-configured egress outside the buyer request path (at boot and on the periodic anchor cycle): the TSA backend (MCP_ANCHOR_SINK=tsa) submits the ledger head hash to the configured RFC 3161 authority; the S3 backend (MCP_ANCHOR_SINK=s3) writes the anchor record to the configured Object Lock bucket; a custom sink module (MCP_ANCHOR_SINK=<module>) runs operator-supplied code. The default local anchor backend performs no external network call. Destinations come only from operator configuration — no buyer input can choose one. This surface is pinned by tests/egress-surface.test.ts: a new outbound capability fails CI until it is declared on purpose.

Why not just use the GAM API directly?

ApproachData exposureWritabilityAuditabilityAI-agent friendly
Raw GAM APIEverything in the accountFull CRUDLogging onlyPoor (SOAP/REST, no MCP)
OpenRTB bid requestsUser-level data, floor pricesBid-onlyNonePoor
This serverFamilies + availability checks from the live forecastBuyer's own soft commitment only (no GAM writes)Hash-chained ledgerNative 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).

GAM connection — forecast only: the live adapter (src/forecast/gam-source.ts) reads availability from GAM's ForecastService when gam.json is present. Catalog families and their GAM targeting (ad units, sizes, environment) are still mapped by hand in config, and prices remain static list prices. Without gam.json the forecast is synthetic (or seeded) and says so (synthetic: true). 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.

LimitationAnchorStatusClosed by
Attribution (buyer_id / request_id) is stored per entry but sits outside the chain's tamper-evidence hashaudit/event.tsDesign decision, not a defect — traceability vs. erasability (ADR-4)—
Head-hash anchor rewrote its whole file each write (writeFileSync) — not append-only, no external WORMaudit/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 destinationaudit/anchor-tsa.ts, audit/anchor-s3.tsOpen — deployment boundary (infra act)—
client_request_id (replay guard) was optional; omitting it bypassed SEC-GATE-3src/server.tsClosed in v0.9.0 — required by default on every authenticated surface (fail-closed); MCP_REQUIRE_IDEMPOTENCY_KEY=0 is an explicit operator opt-out#82
No TLS in transit (a reverse proxy is expected to terminate)—Open — deployment boundary—
revoke_intent is not covered by the rate-limit stagesrc/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 packagepackage.json files✅ Closed 2026-08 — ships as the gam-seller-dsr bin#78
Ledger loaded fail-open — a corrupt file reset to an empty chainaudit/ledger.ts✅ Closed 2026-08-07#65
Chain integrity not verified before serving on startupsrc/server.ts✅ Closed 2026-08-07#65

Architecture

See docs/ARCHITECTURE.md for the module map and data-flow diagrams.

Key modules:

ModuleRole
src/server.tsMCP tool definitions + request pipeline
src/policy/Default-Deny engine, entitlement store, surface allowlist/denylist, per-tool disclosure schemas
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 + sources: synthetic, seeded, live GAM snapshot
src/gam/Service-account OAuth + minimal Ad Manager SOAP client (read-only)
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.

Two gates: who may call a tool, and what it may return. The policy layer works on surface labels: each authenticated tool declares one allowed surface when it is registered, and exact pricing, deal IDs, raw availability numbers, cross-buyer state, real inventory holds (soft-lock) and any ad-server write are permanently on the denylist. That label check does not look at the response, so it cannot on its own stop a tool under an allowed label from returning something it shouldn't. The disclosure gate does: every authenticated tool must declare a strict response schema (src/policy/disclosure.ts), and a response carrying any undeclared field is withheld and replaced by a generic INTERNAL_ERROR (counted on mcp_disclosure_rejected_total). A new tool cannot be registered without that schema, so adding a buyer-visible field means changing it in one reviewable file. 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.

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 recommendationThis node
Protection by design and by defaultDefault-Deny: every surface denied unless an explicit entitlement grants access
Record and document agent actionsAppend-only hash-chained audit ledger; every allow/deny recorded before the response is sent
Control what leaves toward third parties, and with what traceabilityBuyer-facing disclosure allowlist (SEC-GATE-*): exact pricing, deal IDs and raw availability are permanently blocked from responses. Network egress allowlist: the only outbound connections are the operator-opted audit anchors and the operator-opted GAM forecast refresh (see Network egress above), pinned by CI
Govern agent memory with purpose and retention rulesDSR 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:

PropertyCurrent valueMeaning
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_contactfrom deployment.jsonContact for data-subject requests
controller_modelfrom deployment.jsonPublisher's declared controller role
audit_retentionfrom deployment.jsonHot/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

bash
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. Operator commands ship in the npm package and the container image as the gam-seller-admin bin (issue-token, revoke-token, dsr …; gam-seller-dsr remains as an alias for the DSR commands), so no checkout is needed. They write the node's state and therefore run only while the node is stopped: a running node owns its state directory (owner lease) and the command is refused with exit code 3. The scripts/*.ts entries are dev wrappers over the same code.

Roadmap

See the open issues for the full roadmap. Highlights:

  • GAM inventory mapping — derive family targeting from GAM ad units/placements instead of hand-written gam.json
  • 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.

來源:README.md,提交 e6a8bce

工具

0
SourceWeft 尚未在沙箱中啟動過該軟體包,因此其工具未知。

版本歷史

4
  1. v0.11.1最新Sep 30, 2026
  2. v0.10.0Sep 29, 2026
  3. v0.9.0Sep 29, 2026
  4. v0.8.13Sep 16, 2026

評分

已滿足適用檢查項的 50%

該評分衡量此收錄條目的完整程度,依據註冊中心條目、端點與公開來源計算得出。它不是對伺服器本身的評價,也不表示伺服器安全或目前可用:可連通性是帶日期的檢查結果,評分也不包含安裝量或使用者評價。

  • 近 30 天內可連通25 分

    請保持端點能夠回應 MCP 握手;SourceWeft 每 30 天探測一次。

  • 已宣告認證方式10 分

    請在註冊中心條目中宣告憑證請求標頭,或保持端點能夠回應探測,以便判定其認證方式。

  • 已列出工具15 分

    請讓伺服器在無憑證時回應 tools/list,或發布可在沙箱中無憑證啟動的軟體包。

  • 描述必需15 分
  • 圖示10 分
  • 文件15 分
  • 授權條款10 分