@adcp/sdk API Reference - v14.0.0-beta.6
    Preparing search index...

    Interface ServeOptions

    interface ServeOptions {
        port?: number;
        path?: string;
        allowedHosts?: string[];
        allowedOrigins?: string[];
        maxRequestBytes?: number;
        onListening?: (url: string) => void;
        readinessCheck?: () => Promise<void>;
        taskStore?: TaskStore;
        taskScope?: (
            principal: AuthPrincipal,
            request: IncomingMessage,
            endpointScope: string,
        ) => string;
        publicUrl?: string | ((host: string) => string);
        authenticate?: Authenticator;
        protectedResource?:
            | ProtectedResourceMetadata
            | ((host: string) => ProtectedResourceMetadata);
        reuseAgent?: boolean;
        trustForwardedHost?: boolean;
        preTransport?: (
            req: IncomingMessage & { rawBody?: string },
            res: ServerResponse<IncomingMessage>,
        ) => Promise<boolean>;
    }
    Index

    Properties

    port?: number

    Port to listen on. Defaults to PORT env var or 3001.

    path?: string

    HTTP path to mount the MCP endpoint on. Defaults to '/mcp'.

    allowedHosts?: string[]

    Hostnames allowed on framework-owned AdCP server requests, without ports. The guard runs before protocol-era classification, so it protects both legacy and MCP 2026-07-28 traffic. Defaults to the hostname from publicUrl, or localhost-only when no public URL is set. Public and reverse-proxied deployments must configure publicUrl or this allowlist; include the upstream Host too when a proxy rewrites it.

    allowedOrigins?: string[]

    Origin hostnames allowed on browser-originated framework AdCP requests, across both protocol eras. Requests without Origin pass. Defaults to allowedHosts.

    maxRequestBytes?: number

    Maximum buffered MCP request body size used for era classification. Defaults to 2 MiB. Set explicitly when an AdCP extension legitimately carries larger JSON payloads.

    onListening?: (url: string) => void

    Called when the server starts listening.

    readinessCheck?: () => Promise<void>

    Async check called before the server begins accepting connections. Throw to abort boot — serve() logs the error and calls process.exit(1), so the server never enters listen mode and no traffic arrives during the check. Use to validate external dependencies (database pools, credential fetches) so a misconfigured deployment fails loudly at startup rather than silently on the first live request.

    serve(createAgent, {
    readinessCheck: () => store.probe(),
    });
    taskStore?: TaskStore

    Custom task store. Defaults to a shared InMemoryTaskStore.

    The default InMemoryTaskStore only evicts tasks that have a TTL set. For long-running servers, always set ttl when creating tasks, or provide a store with automatic eviction to prevent unbounded memory growth.

    taskScope?: (
        principal: AuthPrincipal,
        request: IncomingMessage,
        endpointScope: string,
    ) => string

    Bind all TaskStore operations for a request to a stable, server-controlled scope derived from the authenticated principal. Use this with shared persistent task stores when the legacy HTTP transport is stateless.

    The resolver's third argument is the canonical, server-controlled endpoint scope (not the raw Host header). It requires authenticate, must return a non-empty string, and cannot be combined with reuseAgent because a reused MCP server retains the TaskStore supplied when it was first constructed. It also cannot be combined with a TaskMessageQueue: queued messages do not carry the principal scope and could otherwise cross ownership boundaries. The factory must return a server built by createAdcpServer() or createTaskCapableServer() so queue absence can be verified; directly constructed raw McpServer instances fail closed. An explicit scope-enforcing taskStore is required because the SDK's default in-memory store ignores session IDs. Client-supplied MCP session IDs are ignored by the scoped facade.

    serve(createAgent, {
    authenticate: verifyBearer({ jwksUri, issuer, audience }),
    taskStore: new PostgresTaskStore(pool),
    taskScope: taskScopeFromPrincipal,
    });
    publicUrl?: string | ((host: string) => string)

    Canonical public URL of this MCP endpoint (e.g. https://my-agent.example.com/mcp). Required when protectedResource is configured — the RFC 9728 resource field, the RFC 6750 resource_metadata URL on 401 challenges, and the JWT audience your tokens must carry are all derived from it. Setting this defends against attacker-controlled Host header phishing: without it, the server would advertise whatever host a caller happened to send.

    Must be an absolute https:// URL whose path matches the mount path.

    Multi-host. Pass a function (host) => string when one process fronts multiple hostnames (white-label publishers, multi-brand adapters). The resolver receives a lowercased, WHATWG-canonical hostname with no port and must be side-effect-free and idempotent. Its returned URL is cached in a 128-host process-local LRU and used as the RFC 9728 resource, the 401-challenge resource_metadata, and the JWT audience for that host. Each resolved URL's path must match the mount path, same as the static form. The host argument is untrusted request input: allowlist it before deriving a URL and throw UnknownHostError for unknown hosts. Evicted hosts are resolved again on their next request. Setting trustForwardedHost is recommended when behind a proxy.

    authenticate?: Authenticator

    Authentication middleware applied to every request. When configured, missing or invalid credentials produce a 401 with a compliant WWW-Authenticate header — no request reaches the MCP transport without passing. Use helpers from ./auth: verifyApiKey, verifyBearer, or anyOf(verifyApiKey(...), verifyBearer(...)).

    protectedResource?:
        | ProtectedResourceMetadata
        | ((host: string) => ProtectedResourceMetadata)

    Advertise OAuth 2.0 protected-resource metadata at /.well-known/oauth-protected-resource<mountPath>. Requires publicUrl.

    Pass a function (host) => ProtectedResourceMetadata for multi-host deployments whose authorization servers or supported scopes vary per hostname (e.g., each white-label seller has its own AS). The resolver must be side-effect-free and idempotent. Results are cached in the same 128-host process-local LRU as publicUrl; evicted hosts are resolved again on their next request. The host argument is untrusted request input: use the same allowlist as publicUrl and throw UnknownHostError for unknown hosts. The static form still works when every host uses the same PRM body.

    reuseAgent?: boolean

    Reuse the agent returned by the factory across requests instead of closing it after each. Default false (one fresh server per request — the pre-existing behavior).

    When true, serve() DOES NOT call agentServer.close() between requests. The factory is still called on every request; it's the caller's responsibility to cache AdcpServer instances (typically keyed on ctx.host) and return the cached instance when possible. Concurrent requests to the same cached server are serialized per instance — McpServer.connect() rejects when a transport is already attached, so the framework wraps the connect→handleRequest→release cycle in a per-instance async mutex. Requests for DIFFERENT cached servers still run in parallel.

    Use when createAdcpServer(...) setup cost (tool registration, handler wiring) is a measurable portion of request latency — common in multi-host deployments with many tools per host. Trade-off: throughput per unique host drops to 1 in flight at a time. For higher concurrency per host, cache a small pool of servers in the factory and round-robin.

    Cleanup is the caller's responsibility: listen for the process shutdown signal and call close() on every cached server to release MCP Tasks timers, HTTP keepalives, etc.

    Incompatible with long-lived server→client streams. MCP's Protocol._onclose aborts in-flight handlers and clears progress tokens when the transport closes. In stateless HTTP mode (the default this helper uses) each request is already single-response, so this is a no-op concern. Don't enable reuseAgent if you eventually wire a transport that keeps an SSE channel open across multiple logical requests.

    trustForwardedHost?: boolean

    Trust X-Forwarded-Host and RFC 7239 Forwarded: host=... for host resolution and for reconstructing the public URL on 401 challenges. Default false — an attacker-controlled header can't flip the advertised OAuth resource URL unless you opt in.

    Your proxy must OVERWRITE the forwarded headers on ingress, not append. The framework trusts the first (left-most) entry in a chain. With an overwriting proxy that's the sanitized value; with an appending proxy it's whatever the client sent — an attacker picks it. Common behavior:

    • Overwrite (safe to trust): Fly.io, Cloud Run, GCP HTTPS LB.
    • Append (NOT safe without extra config): AWS ALB default, nginx default. These need proxy_set_header X-Forwarded-Host $host; or equivalent before enabling this flag.

    Verify your proxy's behavior against a request that already has X-Forwarded-Host: attacker.example in it before turning this on.

    preTransport?: (
        req: IncomingMessage & { rawBody?: string },
        res: ServerResponse<IncomingMessage>,
    ) => Promise<boolean>

    Pre-MCP middleware — runs after authentication but before MCP transport is connected. Intended for transport-layer concerns like RFC 9421 request-signature verification: the agent's body is already buffered into (req as any).rawBody before the middleware fires so signature verifiers can hash it without racing the transport's own body read.

    Return true to signal the middleware handled the response (e.g. a 401 with WWW-Authenticate); the transport is skipped. Return false to continue into MCP dispatch.

    Throwing from the middleware produces a 500 with a generic body.