v5.10.0
Minor Changes
-
8f9260b: feat(server): request validation defaults to
'warn'outside productioncreateAdcpServer({ validation: { requests } })previously defaulted to
'off'everywhere. It now defaults to'warn'when
NODE_ENV !== 'production', mirroring the asymmetric default already in
place forresponses('strict'in dev/test,'off'in production).Production behaviour is unchanged: the default stays
'off'when
NODE_ENV === 'production', so prod request paths pay no AJV cost.What operators will see: in dev/test/CI, each incoming request that
doesn't match the bundled AdCP request schema logs a single
Schema validation warning (request)line through the configured
logger, with the tool name and the field pointer. Nothing is rejected —
the request still flows to the handler exactly as before. Node's test
runner does not setNODE_ENV, so suites running undernode --test
fall into the dev/test bucket and will start emitting these warnings.How to opt out: pass
validation: { requests: 'off' }on the server
config, or setNODE_ENV=productionfor the process.Why: keeps request and response defaults symmetric, and prepares seller
operators for upstream AdCP schema tightenings (e.g. adcp#2795, which
introduces a requiredasset_typediscriminator — buyer agents still
on RC3 fixtures will lack it). Surfacing those drifts as warnings
during development beats discovering them in a downstream consumer's
VALIDATION_ERRORafter deploy.Related: #694 (original intent for
requests: 'warn') and #727 A
(response-side default precedent). -
86a0fde: Register the fourth default cross-step assertion
status.monotonic(adcontextprotocol/adcp#2664). Resource statuses observed across storyboard steps MUST transition only along edges in the spec-published lifecycle graph for their resource type. Catches regressions likeactive → pending_creativeson a media_buy, orapproved → processingon a creative asset, that per-step validations cannot detect.Tracked lifecycles (one transition table per resource type, hardcoded against the enum schemas in
static/schemas/source/enums/*-status.jsonin the spec repo, with bidirectional edges listed explicitly):media_buy— forward flowpending_creatives → pending_start → active,active ↔ pausedreversible, terminalscompleted | rejected | canceled.creative(asset lifecycle) — forward flowprocessing → pending_review → approved | rejected,approved ↔ archivedreversible,rejected → processing | pending_reviewallowed on re-sync, no terminals.creative_approval— per-assignment on a package, forwardpending_review → approved | rejected,rejected → pending_reviewallowed on re-sync.account—active ↔ suspendedandactive ↔ payment_requiredreversible, terminalsrejected | closed.si_session— forwardactive → pending_handoff → complete | terminated, terminalscomplete | terminated.catalog_item— forwardpending → approved | rejected | warning,approved ↔ warningreversible,rejected → pendingallowed on re-sync.proposal— one-waydraft → committed.
Observations are drawn from task-aware extractors on
stepResult.response:create_media_buy/update_media_buy/get_media_buys,sync_creatives/list_creatives, nested.packages[].creative_approvals[],sync_accounts/list_accounts,si_initiate_session/si_send_message/si_terminate_session,sync_catalogs/list_catalogs(per-item),get_products(when the response carries aproposal). Unknown tasks produce no observations.State is scoped
(resource_type, resource_id)so independent resources don't interfere. Self-edges (same status re-read) are silent pass. Skipped / errored /expect_error: truesteps don't record observations. Unknown enum values (drift) reset the anchor without failing —response_schemacatches enum violations.Failure output names the resource, the illegal transition, and the two step ids:
media_buy mb-1: active → pending_creatives (step "create" → step "regress") is not in the lifecycle graph.Consumers who need a stricter variant canregisterAssertion(spec, { override: true }).18 new unit tests cover forward flows, terminal enforcement, bidirectional edges, skip semantics, (resource_type, resource_id) scoping, nested creative_approval arrays, adcp_error-gated observations, enum-drift tolerance.
-
573a176: Improve OAuth ergonomics for
adcp storyboard run.- Fix classification: capability-discovery failures whose error message says
"requires OAuth authorization"(the wordingNeedsAuthorizationErroremits) now classify asauth_requiredwith theSave credentials: adcp --save-auth <alias> <url> --oauthremediation hint, instead of falling through tooverall_status: 'unreachable'with no actionable advice. The keyword list indetectAuthRejectionnow matches"authorization"and"oauth"in addition to401/unauthorized/authentication/jws/jwt/signature verification. - Surface the hint earlier: the OAuth remediation observation now fires whenever the error text looks OAuth-shaped, not only when
discoverOAuthMetadatasuccessfully walks the well-known chain — an agent that 401s before its OAuth metadata is resolvable still gets a useful hint. - Inline OAuth flow:
adcp storyboard run <alias> --oauthnow opens the browser to complete PKCE when the saved alias has no valid tokens, then proceeds with the run. Matches the existingadcp <alias> get_adcp_capabilities --oauthbehavior so the two-step dance (--save-auth --oauththenstoryboard run) is no longer required. Raw URLs still need--save-authfirst; MCP only.
Docs:
docs/CLI.mdanddocs/guides/VALIDATE-YOUR-AGENT.mddocument both flows and add a troubleshooting row for theAgent requires OAuthfailure. - Fix classification: capability-discovery failures whose error message says
-
86ccc99: feat(server): response validation defaults to
'strict'outside productioncreateAdcpServer({ validation: { responses } })previously defaulted to
'warn'whenNODE_ENV !== 'production'. It now defaults to'strict'
in dev/test/CI so handler-returned schema drift fails with
VALIDATION_ERROR(with the offending field path indetails.issues)
instead of logging a warning the caller can silently ignore.Production behaviour is unchanged: the default stays
'off'when
NODE_ENV === 'production', so prod request paths pay no validation
cost. Passvalidation: { responses: 'warn' }to restore the previous
dev-mode behaviour;validation: { responses: 'off' }opts out
entirely.Why: the
compliance:skill-matrixharness has repeatedly surfaced
SERVICE_UNAVAILABLEfrom agents whose responses fail the wire schema.
The dispatcher's response validator catches this drift with a clear
field pointer, one layer that every tool inherits automatically. Making
that the default catches it during handler development rather than in a
downstream consumer.Migration: handler tests that use sparse fixtures (e.g.
{ products: [{ product_id: 'p1' }] }) will start returning
VALIDATION_ERROR. Either fill in the missing required fields to match
the AdCP schema, or setvalidation: { responses: 'off' }on the test
server to keep the fixture intentionally minimal. Note that Node's
test runner does not setNODE_ENV, so test suites running under
node --test(withNODE_ENV=undefined) fall into the dev/test
bucket and will start validating responses — this is intentional.Also: the
VALIDATION_ERRORenvelope'sdetails.issues[].schemaPath
is now gated behindexposeErrorDetails(same policy as the existing
SERVICE_UNAVAILABLE.details.reasonfield). Production responses no
longer leak#/oneOf/<n>/properties/...paths that fingerprint the
handler's internaloneOfbranch selection — buyers still get
pointer,message, andkeyword, which is sufficient to fix a
drifted payload.Closes #727 (A).
-
f64007c: fix(server): tighten handler return types so schema drift fails
tscTwo related tightenings close #727 (B).
1.
AdcpToolMapbrand rights results.acquire_rights,
get_rights, andget_brand_identityhadresult: Record<string, unknown>— a stale scaffold from before the response types were
code-generated. Replaced with the proper generated types:-
acquire_rights→AcquireRightsAcquired | AcquireRightsPendingApproval | AcquireRightsRejected -
get_rights→GetRightsSuccess -
get_brand_identity→GetBrandIdentitySuccess2.
DomainHandlerreturn type. The handler return union
previously included| Record<string, unknown>as a general escape
hatch, so any handler could return any shape. Sparse returns like
{ rights_id, status: 'acquired' }passedtscand only failed at
wire-level validation. Handler return type is now just
AdcpToolMap[K]['result'] | McpToolResponse, so drift fails at
compile time.adcpError(...)still works — it returns
McpToolResponse.Migration. If a handler returns a plain object literal without
spelling out the full success shape,tscwill now flag the drift
with an error like:Type '{ products: [{ product_id: 'p1' }] }' is not assignable to type 'McpToolResponse | GetProductsResponse'. Property 'reporting_capabilities' is missing in type '{ product_id: 'p1' }' but required in type 'Product'.Two ways to fix:
-
Fill in the missing required fields to match the AdCP schema (what
the wire-level validator would have demanded anyway). Use
DEFAULT_REPORTING_CAPABILITIESforProduct.reporting_capabilities
if you don't have seller-specific reporting policy yet. -
If you genuinely need a loose return (e.g. a test fixture), wrap
with a response builder —productsResponse({ ... }),
acquireRightsResponse({ ... }), etc. The builders accept typed
inputs so the drift surfaces there instead of silently passing
through.Reference agents.
test-agents/seller-agent.tsnow uses
DEFAULT_REPORTING_CAPABILITIESon each product (the old code had
a "Use plain objects instead of Product type" comment whose premise
was wrong —reporting_capabilitiesis required, not optional).
test-agents/seller-agent-signed-mcp.tshad a latent bug:
createMediaBuywas readingpkg.package_idfrom the request, but
PackageRequesthas no such field — buyers sendbuyer_refand
the seller mintspackage_idper spec. The handler now mints
crypto.randomUUID()like a real seller would.
-
Patch Changes
-
275fa70:
docs/llms.txtnow includes per-tool response contracts. Each tool section gets a**Response (success branch):**block listing the required + optional fields drawn from the bundled JSON schemas — same format the existing request block uses.Closes the drift path we kept seeing in matrix runs: agents dropped required response fields (missing
format_idoncreative_manifest, plural-variant hallucinations likecreative_deliveriesforcreatives, missing top-levelcurrency) because the skill examples documented the intent but the full per-field contract lived in the generated schemas and was never surfaced in the llms.txt index Claude actually reads when building. The contract is now one anchored section away:docs/llms.txt#build_creative,docs/llms.txt#get_creative_delivery, etc. — same convention as the llms.txt pattern other projects use.No SDK code change; llms.txt is regenerated via
npm run generate-agent-docs. -
6ae3169: chore(server): migrate McpServer.tool() → registerTool() repo-wide (#705)
Replaces every use of the MCP SDK's deprecated
McpServer.tool(...)
overload with the supportedregisterTool(name, config, handler)form.
Behavior is unchanged;tools/listoutput is identical aside from a
cleaner path to the same metadata.What moved
src/lib/server/create-adcp-server.ts— the AdcpToolMap registration
loop andget_adcp_capabilitiesnow useregisterTool, with
annotationsdeclared at register time instead of via a post-hoc
.update()call.src/lib/server/test-controller.ts,src/lib/testing/comply-controller.ts
—comply_test_controllerregistration.src/lib/testing/stubs/governance-agent-stub.ts— all five tools
(get_adcp_capabilities,sync_plans,check_governance,
report_plan_outcome,get_plan_audit_logs).examples/error-compliant-server.ts— the canonical seller template.- JSDoc, prose, and README-ish comments updated to the new form.
outputSchemadeliberately not wired on framework toolsThe MCP SDK's client-side
callToolvalidatesstructuredContent
against the declaredoutputSchemawhenever structuredContent is
present — regardless ofisError
(@modelcontextprotocol/sdk/dist/esm/client/index.js:504). AdCP's
adcpError()envelope carriesstructuredContent: { adcp_error: {...} }
alongsideisError: true, which would fail every client-side outputSchema
check (the error shape doesn't match the success schema). Until the SDK
gates that client-side check on!isError(the server-side validator
already does), framework-registered tools are migrated without
outputSchema. Response drift is caught by the dispatcher's AJV
validator (#727) instead, andcustomToolsmay opt in explicitly via
customTools[*].outputSchema— validated by a new regression test.Closes #705.
-
275fa70: Skill files now point Claude at
docs/llms.txt#<tool>for per-tool field contracts instead of duplicating them inline.Callout wording is imperative, not descriptive (per prompt-engineering review): "Before writing any handler's return statement, fetch
docs/llms.txtand grep for#### \<tool_name>``..." Replaces the earlier passive "contracts live at X" phrasing that relied on Claude optionally following the pointer. Safety-net sentence reframed as permission ("write the obvious thing and trust the contract") rather than threat.Grep instructions match how agents actually find sections: Markdown anchors resolve on GitHub but Claude reading the raw file searches for
#### \tool_name``. The callout names that pattern directly.build-creative-agent slim collapses verbose response-shape blocks into a 4-column handler-binding table:
Tool | Handler | Contract | Gotchas. The Contract column carries a direct anchor link per tool so Claude is likelier to click the one adjacent to the row it's reading than a general pointer three lines up. Asset-shape bullets stay inline (most-drifted fields historically). Net -94 lines on that skill.Other 7 skills get the pointer callout only — structural slims deferred until matrix v12 signal is in. Two variables (pointer + slim) on one skill lets us disambiguate outcomes.
Lands on top of strict validation default in dev (#727/#757) and the llms.txt response-contract generator (#761). Together: llms.txt is canonical, skills are narrative + gotchas, strict validation catches residual drift at call site.