OptionaladcpOptionalresolveResolve an account from an AccountReference.
Called on every request that has an account field.
Return null if the account doesn't exist — framework responds ACCOUNT_NOT_FOUND.
The second argument carries the AdCP toolName and, when serve() is
configured with authenticate, the caller's authInfo. Adapters that
need the caller's upstream platform token to look up the account can
read it from ctx.authInfo here and attach anything they want to the
resolved account.
Single-argument resolvers (async (ref) => ...) are valid — TypeScript
allows a shorter parameter list.
Do not persist authInfo outside the returned account object. The
framework makes no isolation guarantees about values closed over by the
resolver; caching authInfo into shared state can leak the caller's
principal across requests.
OptionalresolveResolve an account when the wire request doesn't carry one.
For tools whose request schema lacks an account field
(provide_performance_feedback, list_creative_formats, the
tasks/get polling path, etc.), the framework can't extract a wire
ref. When this resolver is configured, the framework calls it with the
caller's authInfo instead so single-tenant agents (resolution: 'derived')
and principal-keyed agents (resolution: 'implicit') still get a
tenant-scoped ctx.account.
Returns null when no account can be derived. The handler then runs
with ctx.account undefined — appropriate for tools that legitimately
don't need tenant scoping (publisher-wide format catalogs).
OptionalagentBuyer-agent identity registry — Phase 1 of #1269. Optional. When
configured, framework calls agentRegistry.resolve(authInfo) once per
request after authInfo is populated and before resolveAccount. The
resolved BuyerAgent is threaded through ctx.agent to
resolveAccount, resolveAccountFromAuth, resolveSessionKey, and
the specialism handlers.
Adopters construct via BuyerAgentRegistry.signingOnly,
BuyerAgentRegistry.bearerOnly, or BuyerAgentRegistry.mixed.
When omitted, ctx.agent stays undefined and the framework's request
flow is unchanged — strict opt-in.
Behavior:
BuyerAgent → framework freezes the record (and
its billing_capabilities Set) and sets ctx.agent.null → ctx.agent stays undefined; dispatch
continues. Status enforcement (suspended/blocked) and per-agent
billing rejection are Stage 4 / Phase 2 work.SERVICE_UNAVAILABLE. Inner
error logged server-side.Phase 1 ships the seam; the framework consumes the resolved record but does not yet enforce billing capabilities or status. Phase 2 (#1292) wires those once the SDK pin moves to AdCP 3.1.
OptionalresolveDerive a session-scoping key from the request. Populates ctx.sessionKey
so handlers don't re-implement key derivation (tenant, brand, publisher
account id, etc.). Called after resolveAccount, so the resolved account
is available.
Return undefined to leave ctx.sessionKey unset (e.g., for anonymous
or public tools).
OptionalexposeWhen true, framework-produced SERVICE_UNAVAILABLE errors include the
underlying err.message in details.reason (helpful in dev, but can leak
DB driver messages, file paths, or schema info to remote callers).
Defaults:
NODE_ENV === 'production' → false (safe default for live agents).true (dev/test/CI surface the cause chain so
SERVICE_UNAVAILABLE: encountered an internal error becomes
SERVICE_UNAVAILABLE: Cannot find module '@adcp/sdk/foo'.
Matrix runs spent weeks on opaque SU errors before this default flipped).Explicit exposeErrorDetails: true | false always wins.
OptionalloggerLogger for framework decisions. Defaults to no-op.
OptionalstateState store for persisting domain objects across requests. Defaults to InMemoryStateStore. Use PostgresStateStore for production.
OptionalmediaOptionalsignalsOptionalcreativeOptionalgovernanceOptionalaccountsOptionaleventOptionalsponsoredOptionalbrandOptionalcapabilitiesExplicit capabilities overrides (merged on top of auto-detected).
OptionalidempotencyIdempotency store for mutating requests. When configured, the framework:
idempotency_key on every mutating request (returns
INVALID_REQUEST when missing)(principal, key, payload)
and injects replayed: true on the envelopeIDEMPOTENCY_CONFLICT for same-key-different-payloadIDEMPOTENCY_EXPIRED when the key is past the TTLadcp.idempotency.replay_ttl_seconds on get_adcp_capabilitiesScoping: by default uses ctx.sessionKey as the principal. Override
via resolveIdempotencyPrincipal.
Pass the literal string 'disabled' to suppress idempotency
enforcement end-to-end: schema validation tolerates a missing
idempotency_key on mutating tools, the replay/conflict middleware
is skipped, and the missing-store guardrail log is silenced. Intended
for non-production test fleets that don't model idempotency replay
— production servers should always wire a real store. The framework
logs an info-level warning at construction when this mode is used
outside NODE_ENV=test so the choice is visible.
OptionalresolveDerive the idempotency principal from the handler context, the
request params, and the tool name. Defaults to ctx.sessionKey. Two
buyers that share a sessionKey would share cache entries — if that's
not what you want, return something more specific (e.g., an
operator_id) from this hook.
Receives params and toolName so callers can fold request-shape
identity into the principal when needed (e.g., scoping by a custom
tenant header). For per-session scoping (si_send_message), the
framework already folds params.session_id into the scope tuple —
the principal is still the authenticated buyer.
OptionalinstructionsServer-level prose surfaced on MCP initialize. Two forms:
(ctx: SessionContext) => string | undefined — re-evaluated
each time createAdcpServer is called. Under the canonical
serve({ reuseAgent: false }) flow (the default) the factory
runs per HTTP request, which under streamable-HTTP MCP is per
session — so the closure can surface tenant-shaped prose (per-buyer
brand manifests, storefront copy, "premium vs standard" partner
guidance).Eval moment. Strictly: once per createAdcpServer invocation.
serve({ reuseAgent: false }) makes that "per HTTP request, which is
per session for streamable-HTTP MCP." Custom transports / hand-rolled
dispatch must invoke createAdcpServer per session themselves to
preserve the per-session semantic. reuseAgent: true would fire the
function once for the lifetime of the shared agent — which defeats
the purpose, so serve() refuses that combination at the first
request.
SessionContext is reserved. authInfo and agent are typed for
forward compatibility but currently always undefined — the framework
does not yet plumb auth/registry state into the factory. Use closures
captured in your factory's HTTP-scoped state for tenant identity today;
ctx.agent/ctx.authInfo reads silently return undefined and ship
empty prose to prod. The function body will pick up populated fields
when the framework wires them through.
serve(({ taskStore, host }) => createAdcpServer({
// host is HTTP-scoped — captured in the closure, NOT from ctx.
instructions: () => brandManifests.get(host)?.intro ?? defaultProse,
// ... rest of config
}));
Async functions are supported. The framework awaits the returned
Promise during MCP initialize — the session does not proceed until
the promise settles. A slow fetch adds session-establishment latency,
not per-tool latency; add a timeout inside your function if needed
(e.g. Promise.race([fetchProse(), timeout(2000)])). A rejected
promise is governed by onInstructionsError: 'skip' (default) logs
and sends no instructions; 'fail' causes the initialize handshake
to fail, dropping the session.
OptionalonBehavior when a function-form instructions callback throws. Defaults
to 'skip' — best-effort prose (brand manifests, marketing copy)
should not kill the buyer's session on a registry fetch failure.
Set 'fail' for adopters whose instructions carry load-bearing
policy. See OnInstructionsError.
OptionaltaskOptionaltaskOptionalwebhooksWebhook-emission config. When set, ctx.emitWebhook is populated on
every handler's context — handlers post signed, retried,
idempotency-stable webhooks without hand-rolling the pipeline. Omit
if your server never emits webhooks.
Provide exactly one of signerKey (in-process JWK) or signerProvider
(KMS-backed async signing). The signing key or provider key MUST have
adcp_use: "webhook-signing" — a request-signing key is a conformance
violation per adcp#2423 (key purpose discriminator). Publishers publishing
their JWKS at the jwks_uri on brand.json's agents[] entry reuse the
same key across every buyer they deliver to.
OptionalsignedAuto-wire the RFC 9421 request-signature verifier onto the HTTP transport.
When set together with capabilities.specialisms containing
signed-requests, serve() mounts the verifier as preTransport so
every inbound MCP request is verified before JSON-RPC dispatch. Setting
one without the other throws at construction time — the spec requires
the specialism claim and a working verifier to stay in lock-step.
Omit entirely when the seller doesn't verify inbound signatures. Servers
that wire the verifier manually via serve({ preTransport }) are
unaffected — auto-wiring only kicks in through this field.
OptionalvalidationSchema-driven validation of requests and responses against the bundled
AdCP JSON schemas. When enabled, the dispatcher rejects bad requests
with VALIDATION_ERROR before the handler runs and catches drift in
handler-returned responses before they leave the server.
Defaults:
NODE_ENV === 'production' → both sides 'off' (zero overhead
in prod; trust the handler after its test suite has exercised it).responses: 'strict', requests: 'warn'.
Strict on responses turns handler-returned drift into a
VALIDATION_ERROR with the offending field path — surfaces the
"handler returned a sparse object that fails the wire schema" class
of bug at development time instead of letting it ship and surface
downstream as a cryptic SERVICE_UNAVAILABLE or oneOf
discriminator failure. Warn on requests logs incoming payloads that
don't match the bundled AdCP schema but still dispatches, so
upstream schema tightenings show up as diagnostics without breaking
clients that haven't caught up.Pass an explicit validation: { requests: 'off', responses: 'off' } to
override the dev-mode default. Set responses: 'warn' to keep the
logger diagnostic without failing the request — useful while
migrating a handler set from sparse fixtures to spec-compliant
responses. (The logger warning fires in both 'warn' and 'strict'
modes; 'strict' additionally promotes the failure to a
VALIDATION_ERROR envelope.)
Per-side modes:
requests: 'strict' — reject malformed requests with VALIDATION_ERROR.
'warn' — log a warning, allow the handler to run.
'off' — skip.responses: 'strict' — handler-returned drift throws (dev/test canary).
'warn' — log a warning, return the response unchanged.
'off' — skip.Cost: one AJV compile per tool on cold start, one validator invocation per call. The dev-mode default trades that for field-level diagnostics when handlers drift from the wire contract.
OptionalcredentialReject buyer requests that smuggle credential-shaped keys through
the args bag. Closes the bug class observed in storefront fan-out
paths where keys like <platform>_access_token (top-level, in
context, or in ext) flow through to upstream calls under the
storefront's TLS / IP reputation — confused-deputy by default.
Modes:
'lax' (default) — no scan; preserves existing behavior.'authInfo-only' — scan args for credential-shaped keys at any
depth and reject with INVALID_REQUEST. Credentials must arrive
on authInfo (resolved by the framework's authenticator) and
never on the args bag.Pass the object form for pattern customization or per-tool overrides:
credentialPolicy: {
policy: 'authInfo-only',
patterns: { extend: [/^bearer$/i, /credentials/i] },
tools: { activate_signal: 'lax' }, // legitimate buyer-creds tool
}
Default patterns: _access_token$, _secret$, _password$,
accessToken, refreshToken. The next platform-specific vector
lives in adopter config, not in the SDK.
OptionalcustomRegister tools outside AdcpToolMap. Keys are the public tool names; values follow AdcpCustomToolConfig.
Gives sellers a declarative extension point without reaching for the
getSdkServer() escape hatch. Typical callers:
*_collection_list family).comply_test_controller — prefer
registerTestController which wraps this).Custom tools bypass the framework's spec-tool pipeline. No
idempotency middleware, no governance pre-check, no account
resolution, no response wrapping. The handler receives SDK-validated
args and must return a CallToolResult. Call framework helpers
(checkGovernance, adcpError, capabilitiesResponse, …) from
inside the handler if you need those behaviors.
Name collisions with registered AdcpToolMap tools (from mediaBuy,
signals, creative, governance, accounts, eventTracking,
sponsoredIntelligence) or with get_adcp_capabilities throw at
construction time — the spec handler wins by convention.
OptionaltestOpt-in bridge between the comply_test_controller seed store and the
spec-tool pipeline. When getSeededProducts is provided, seeded
products flow into get_products responses on sandbox requests — the
Group A compliance storyboards rely on this end-to-end flow.
Production traffic (no sandbox marker, or a resolved non-sandbox
account) bypasses the bridge entirely; omit the field in production
configs to be explicit about it.
The bridge is one of two mechanisms for closing the seed→read loop in compliance testing. Pick by where your read handlers fetch from — not by seller class:
comply_test_controller.seed_product writes to
your DB; your handler reads from your DB; the seed→read loop
closes naturally. Don't wire the bridge — test mode alone
covers you.comply_test_controller.seed_product is a dead
write for you because the handler will never see it. Wire the
bridge. The real handler still runs first (so a broken upstream
call still fails the conformance gate — adapter exercise is
preserved), and the SDK merges seeded fixtures into the response
after.Either path earns wire-conformance credit when storyboards pass; it
is not a separate certification category. Live-integration credit
requires marker-free passes against a real test surface (sandbox
credentials, real catalog data, real adapter traffic) — independent
of whether the bridge is wired. See docs/guides/VALIDATE-YOUR-AGENT.md
§ "Platform-proxy sellers" for the wiring mechanics, and
adcp-client#1782
plus the upstream taxonomy proposal at
adcontextprotocol/adcp#4593
for the certification model under review.
The bridge is gated by isSandboxRequest(params) && (ctx.account === undefined || ctx.account.sandbox === true). The second clause is the
authority boundary; the first is caller-supplied (account.sandbox or
context.sandbox on the request body) and is NOT a trust boundary on
its own. If you register testController WITHOUT configuring
resolveAccount (so ctx.account stays undefined), an attacker who
sets account.sandbox = true on production traffic gets seeded
fixtures merged into responses and the _bridge marker stamped.
Production deployments that register testController MUST:
resolveAccount so the framework can refuse the merge
when the resolved account is not flagged sandbox: true, ortestController entirely outside test / staging environments.The createAdcpServerFromPlatform flow already enforces this via the
sandbox-authority gate (see Phase 2 of #1435 — resolved-account mode
is the trust boundary, not buyer-supplied account.sandbox). The
direct createAdcpServer flow does not; adopters wiring the bridge
here are responsible for the gate. See the top-of-file JSDoc on
TestControllerBridge for the full adopter-responsibility note (#1779).
See src/lib/server/test-controller-bridge.ts for the sandbox-marker
predicate and the merge contract.
import { createAdcpServer, bridgeFromTestControllerStore } from '@adcp/sdk/server/legacy/v5';
const seedStore = new Map<string, unknown>();
const server = createAdcpServer({
mediaBuy: { getProducts: handleGetProducts },
testController: bridgeFromTestControllerStore(seedStore, {
delivery_type: 'guaranteed',
channels: ['display'],
}),
});
AdCP protocol version this server speaks. Defaults to ADCP_VERSION — the GA version the SDK ships against. Override to pin to an older stable (e.g.,
'3.0.0') or opt into a beta channel ('3.1.0-beta.1') once that registry ships.Not the same as
version(the publisher's app version, e.g.,'1.4.2').Stage 2 plumbs the option through and validates it at construction time; cross-major pins (e.g.
'4.0.0-beta.1'while the SDK ships against major 3) throwConfigurationError. Stage 3 wires per-instance schema/validator selection off this field.Typed as
AdcpVersion | (string & {})so editors autocomplete canonical values from COMPATIBLE_ADCP_VERSIONS while still accepting forward-compatible strings.