@adcp/sdk API Reference - v7.9.0
    Preparing search index...

    Interface AdcpServerConfig<TAccount>

    interface AdcpServerConfig<TAccount = unknown> {
        name: string;
        version: string;
        adcpVersion?:
            | "v3"
            | "3.0.12"
            | "v2.5"
            | "v2.6"
            | "3.0.0-beta.1"
            | "3.0.0-beta.3"
            | "3.1.0-beta.1"
            | "3.0.0"
            | "3.0.1"
            | "3.0.2"
            | "3.0.3"
            | "3.0.4"
            | "3.0.5"
            | "3.0.6"
            | "3.0.7"
            | "3.0.8"
            | "3.0.9"
            | "3.0.10"
            | "3.0.11"
            | string & {};
        resolveAccount?: (
            ref: AccountReference,
            ctx: ResolveAccountContext,
        ) => Promise<TAccount | null>;
        resolveAccountFromAuth?: (
            ctx: ResolveAccountContext,
        ) => Promise<TAccount | null>;
        agentRegistry?: BuyerAgentRegistry;
        resolveSessionKey?: (
            ctx: SessionKeyContext<TAccount>,
        ) => string | Promise<string | undefined> | undefined;
        exposeErrorDetails?: boolean;
        logger?: AdcpLogger;
        stateStore?: AdcpStateStore;
        mediaBuy?: MediaBuyHandlers<TAccount>;
        signals?: SignalsHandlers<TAccount>;
        creative?: CreativeHandlers<TAccount>;
        governance?: GovernanceHandlers<TAccount>;
        accounts?: AccountHandlers<TAccount>;
        eventTracking?: EventTrackingHandlers<TAccount>;
        sponsoredIntelligence?: SponsoredIntelligenceHandlers<TAccount>;
        brandRights?: BrandRightsHandlers<TAccount>;
        capabilities?: AdcpCapabilitiesConfig;
        idempotency?: IdempotencyStore | "disabled";
        resolveIdempotencyPrincipal?: (
            ctx: HandlerContext<TAccount>,
            params: IdempotencyPrincipalParams,
            toolName: keyof AdcpToolMap,
        ) => string | undefined;
        instructions?:
            | string
            | ((ctx: SessionContext) => MaybePromise<string | undefined>);
        onInstructionsError?: OnInstructionsError;
        taskStore?: TaskStore;
        taskMessageQueue?: TaskMessageQueue;
        webhooks?: WebhooksConfig;
        signedRequests?: SignedRequestsConfig;
        validation?: { requests?: ValidationMode; responses?: ValidationMode };
        credentialPolicy?: CredentialPolicy;
        customTools?: Record<string, AdcpCustomToolConfig<any, any>>;
        testController?: TestControllerBridge<TAccount>;
    }

    Type Parameters

    • TAccount = unknown
    Index

    Properties

    name: string
    version: string
    adcpVersion?:
        | "v3"
        | "3.0.12"
        | "v2.5"
        | "v2.6"
        | "3.0.0-beta.1"
        | "3.0.0-beta.3"
        | "3.1.0-beta.1"
        | "3.0.0"
        | "3.0.1"
        | "3.0.2"
        | "3.0.3"
        | "3.0.4"
        | "3.0.5"
        | "3.0.6"
        | "3.0.7"
        | "3.0.8"
        | "3.0.9"
        | "3.0.10"
        | "3.0.11"
        | string & {}

    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) throw ConfigurationError. 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.

    resolveAccount?: (
        ref: AccountReference,
        ctx: ResolveAccountContext,
    ) => Promise<TAccount | null>

    Resolve 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.

    resolveAccountFromAuth?: (
        ctx: ResolveAccountContext,
    ) => Promise<TAccount | null>

    Resolve 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).

    agentRegistry?: BuyerAgentRegistry

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

    • Registry returns a BuyerAgent → framework freezes the record (and its billing_capabilities Set) and sets ctx.agent.
    • Registry returns nullctx.agent stays undefined; dispatch continues. Status enforcement (suspended/blocked) and per-agent billing rejection are Stage 4 / Phase 2 work.
    • Registry throws → framework returns 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.

    resolveSessionKey?: (
        ctx: SessionKeyContext<TAccount>,
    ) => string | Promise<string | undefined> | undefined

    Derive 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).

    exposeErrorDetails?: boolean

    When 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).
    • Otherwise → 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.

    logger?: AdcpLogger

    Logger for framework decisions. Defaults to no-op.

    stateStore?: AdcpStateStore

    State store for persisting domain objects across requests. Defaults to InMemoryStateStore. Use PostgresStateStore for production.

    sponsoredIntelligence?: SponsoredIntelligenceHandlers<TAccount>
    brandRights?: BrandRightsHandlers<TAccount>
    capabilities?: AdcpCapabilitiesConfig

    Explicit capabilities overrides (merged on top of auto-detected).

    idempotency?: IdempotencyStore | "disabled"

    Idempotency store for mutating requests. When configured, the framework:

    • Requires idempotency_key on every mutating request (returns INVALID_REQUEST when missing)
    • Replays the cached response for matching (principal, key, payload) and injects replayed: true on the envelope
    • Returns IDEMPOTENCY_CONFLICT for same-key-different-payload
    • Returns IDEMPOTENCY_EXPIRED when the key is past the TTL
    • Declares adcp.idempotency.replay_ttl_seconds on get_adcp_capabilities

    Scoping: 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.

    resolveIdempotencyPrincipal?: (
        ctx: HandlerContext<TAccount>,
        params: IdempotencyPrincipalParams,
        toolName: keyof AdcpToolMap,
    ) => string | undefined

    Derive 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.

    instructions?:
        | string
        | ((ctx: SessionContext) => MaybePromise<string | undefined>)

    Server-level prose surfaced on MCP initialize. Two forms:

    1. Static string (the historical form) — captured at construction, same value for every session.
    2. Function (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.

    • SessionContext
    • onInstructionsError
    onInstructionsError?: OnInstructionsError

    Behavior 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.

    taskStore?: TaskStore
    taskMessageQueue?: TaskMessageQueue
    webhooks?: WebhooksConfig

    Webhook-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.

    signedRequests?: SignedRequestsConfig

    Auto-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.

    validation?: { requests?: ValidationMode; responses?: ValidationMode }

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

    • When NODE_ENV === 'production' → both sides 'off' (zero overhead in prod; trust the handler after its test suite has exercised it).
    • Otherwise (dev, test, CI) → 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.

    credentialPolicy?: CredentialPolicy

    Reject 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.

    • CredentialPolicy
    • docs/guides/CTX-METADATA-SAFETY.md
    customTools?: Record<string, AdcpCustomToolConfig<any, any>>

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

    • AdCP surfaces whose JSON Schemas haven't landed yet.
    • Governance specialism helpers (*_collection_list family).
    • Test-harness tools (comply_test_controller — prefer registerTestController which wraps this).
    • Seller-specific extensions outside the AdCP spec.

    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.

    testController?: TestControllerBridge<TAccount>

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

    • Handler reads from a store you control (most SSPs, most creative agents). 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.
    • Handler reads from a system you don't control (DSPs proxying to Meta/Snap/TikTok, retail-media networks reading retailer catalog APIs, signals agents brokering third-party data marketplaces). 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:

    1. Configure resolveAccount so the framework can refuse the merge when the resolved account is not flagged sandbox: true, or
    2. Omit testController 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'],
    }),
    });