Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/quiet-versions-select.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@adcp/sdk': minor
---

Separate a server's default served AdCP release from its maximum supported release, and expose the immutable selected release across handler contexts.
18 changes: 17 additions & 1 deletion docs/guides/MEDIA-BUY-3.2-COMPATIBILITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,10 +76,23 @@ const platform = {
createAdcpServerFromPlatform(platform, {
name: 'seller',
version: '1.0.0',
// `adcpVersion` is the newest release explicit callers may select.
adcpVersion: '3.2.0-rc.1',
// Keep callers that omit a version on the established 3.1 contract.
defaultAdcpVersion: '3.1.18',
capabilities: { supported_versions: ['3.1.18', '3.2.0-rc.1'] },
});
```

`adcpVersion` is the server's supported ceiling; `defaultAdcpVersion` is the
release selected when the request has neither `adcp_version` nor
`adcp_major_version`. The default must be bundled, included in the server's
effective advertised releases, and no newer than the ceiling. An explicit 3.2
request still selects 3.2. The selected release is exposed as immutable
`servedAdcpVersion` on standard handler contexts, custom-tool extras,
DecisioningPlatform request contexts, task handoff contexts, account/session
resolvers, and the second argument to `responseEnhancer`.

With the default `mcpToolProfile: 'auto'`, registering any compact lifecycle
handler on a 3.2 server selects the active media-buy profile. The legacy
handlers stay registered and callable but disappear from `tools/list`.
Expand Down Expand Up @@ -624,6 +637,7 @@ trusted namespaces, never a buyer-supplied proposal ID alone.
| Compact-first coordinator | AdCP 3.2 dual-surface | Compact tools are preferred; `preferredLifecycle: 'established'` exercises the compatibility facade | Native by default; the forced established lane has the same explicit boundaries as older sellers. |
| Compact-first coordinator | AdCP 3.2 compact-only | Compact lifecycle tools | Native compact guarantees. |
| Existing AdCP 3.0/3.1 buyer | Dual-surface SDK 14 seller | Hidden legacy names remain directly callable even though compact names are the advertised profile | Existing buyer code does not need lifecycle negotiation. |
| Unversioned caller and explicit AdCP 3.2 caller | Dual-surface SDK 14 seller with `defaultAdcpVersion: '3.1.18'` and a 3.2 `adcpVersion` ceiling | Unversioned discovery and calls use the 3.1 established surface; explicit 3.2 discovery and calls use the compact surface | Unversioned callers are never upgraded implicitly, while the 3.2 feature set remains reachable by explicit opt-in. |

“Same application lifecycle” therefore means one coordinator API and the same
commercial intent wherever that intent is representable. It does **not** mean
Expand All @@ -633,7 +647,9 @@ never label the weaker mutation as equivalent.

The coordinator test matrix covers SDK 14 compact-first callers against v2.5,
3.0, 3.1, 3.2 legacy-only, 3.2 dual-surface (compact preferred and established
forced), and 3.2 compact-only discovery. Honest raw-MCP 3.0.25, 3.1.18, and
forced), and 3.2 compact-only discovery. The dual-surface MCP lane also holds
the maximum at 3.2 while defaulting unversioned discovery and dispatch to 3.1,
then proves an explicit 3.2 caller reaches the compact surface. Honest raw-MCP 3.0.25, 3.1.18, and
3.2 legacy-only fixtures execute direct purchase; pause, resume, cancellation,
and readback; plus request, finalize, decline, accept, post-accept control, and
readback for ordinary legacy proposals while
Expand Down
22 changes: 22 additions & 0 deletions docs/migration-13-to-14.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,28 @@ convergence, webhook retry horizons, and crash-safe continuation generation
replacement. Beta.6 adds coordinated placements, seller-rendered stateful
display, creative component assets, and A2A 1.0 request-signing method names.

### Separate the server default from its supported ceiling

An SDK 14 server can advertise and serve 3.2 without silently moving
unversioned callers off 3.1:

```ts
const server = createAdcpServer({
adcpVersion: '3.2.0-rc.1',
defaultAdcpVersion: '3.1.18',
capabilities: { supported_versions: ['3.1.18', '3.2.0-rc.1'] },
// handlers...
});
```

`adcpVersion` is the maximum supported release; `defaultAdcpVersion` is used
only when the caller supplies no version claim. Explicit 3.2 callers select
3.2. Standard and custom handlers can read the immutable
`servedAdcpVersion`, as can DecisioningPlatform request and task-handoff
contexts. `responseEnhancer` receives the same value in its new optional
second argument. Discovery without a version claim uses the default release,
including MCP `tools/list`, generated capabilities, and A2A agent cards.

### A2A 1.0 peer upgrade

SDK 14's AdCP 3.2 transport requires `@a2a-js/sdk` 1.x. Upgrade the peer
Expand Down
14 changes: 11 additions & 3 deletions src/lib/core/SingleAgentClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { z } from 'zod';
import * as schemas from '../types/schemas.generated';
import type { AgentConfig } from '../types';
import { ADCP_ENVELOPE_FIELDS } from '../types/adcp';
import { parseAdcpMajorVersion, type AdcpVersion } from '../version';
import { parseAdcpMajorVersion, toReleasePrecisionVersion, type AdcpVersion } from '../version';
import {
isAdcpVersionSupported,
isAdcpVersionAtLeast,
Expand Down Expand Up @@ -7993,6 +7993,11 @@ export class SingleAgentClient {
...(options?.signal && { signal: options.signal }),
...(clientRequestTimeoutMs !== undefined && { timeout: clientRequestTimeoutMs }),
};
const discoveryAdcpVersion = this.getWireAdcpVersion();
const mcpListParams =
parseAdcpMajorVersion(discoveryAdcpVersion) >= 3
? { _meta: { adcp_version: toReleasePrecisionVersion(discoveryAdcpVersion) } }
: undefined;
const ensureReadAuthToken = async (): Promise<string | undefined> => {
if (!this.normalizedAgent.oauth_client_credentials) return this.normalizedAgent.auth_token;
const { ensureClientCredentialsTokens, getAgentStorage } = await import('../auth/oauth');
Expand All @@ -8009,7 +8014,7 @@ export class SingleAgentClient {
if (this.normalizedAgent._inProcessMcpClient) {
const mcpClient = this.normalizedAgent._inProcessMcpClient;
const toolsList = await withResponseSizeLimit(maxResponseBytes, () =>
mcpClient.listTools(undefined, mcpRequestOptions)
mcpClient.listTools(mcpListParams, mcpRequestOptions)
);
const tools = toolsList.tools.map(tool => ({
name: tool.name,
Expand Down Expand Up @@ -8076,6 +8081,9 @@ export class SingleAgentClient {
: await withResponseSizeLimit(maxResponseBytes, () =>
tryListModernMCPTools(agent.agent_uri, readAuthToken, this.normalizedAgent.headers, {
authProvider,
...(mcpListParams?._meta.adcp_version !== undefined && {
adcpVersion: mcpListParams._meta.adcp_version,
}),
signal: options?.signal,
requestTimeoutMs: transport?.requestTimeoutMs,
fetchFn: transport?.trustedFetchFn,
Expand All @@ -8101,7 +8109,7 @@ export class SingleAgentClient {
const { client: mcpClient } = await connectMCP(connectOptions);
try {
const toolsList = await withResponseSizeLimit(maxResponseBytes, () =>
mcpClient.listTools(undefined, mcpRequestOptions)
mcpClient.listTools(mcpListParams, mcpRequestOptions)
);

const tools = toolsList.tools.map(tool => ({
Expand Down
2 changes: 2 additions & 0 deletions src/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1341,6 +1341,8 @@ export type {
AdcpServerToolName,
AdcpCapabilitiesConfig,
LegacyAdcpCustomToolConfig,
LegacyAdcpCustomToolHandler,
LegacyAdcpCustomToolHandlerExtra,
McpAppUiMeta,
McpAppMeta,
AdcpMcpResourceDefinition,
Expand Down
13 changes: 9 additions & 4 deletions src/lib/protocols/mcp-modern.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ export interface ModernMCPConnectionOptions {
requestTimeoutMs?: number;
fetchFn?: typeof fetch;
allowPrivateIp?: boolean;
/** SDK-selected AdCP release-precision version for tools/list discovery. */
adcpVersion?: string;
/** Use the v2 SDK's negotiated legacy client instead of handing off to v1. */
handleLegacy?: boolean;
}
Expand Down Expand Up @@ -716,10 +718,13 @@ export async function tryListModernMCPTools(
const listTools = async (connectedClient: Client): Promise<ModernMCPListAttempt> => {
if (connectedClient.getProtocolEra() !== 'modern') return { handled: false };
const resolvedRequestTimeoutMs = resolveClientRequestTimeoutMs(options.requestTimeoutMs);
const result = await connectedClient.listTools(undefined, {
...(options.signal && { signal: options.signal }),
...(resolvedRequestTimeoutMs !== undefined && { timeout: resolvedRequestTimeoutMs }),
});
const result = await connectedClient.listTools(
options.adcpVersion === undefined ? undefined : { _meta: { adcp_version: options.adcpVersion } },
{
...(options.signal && { signal: options.signal }),
...(resolvedRequestTimeoutMs !== undefined && { timeout: resolvedRequestTimeoutMs }),
}
);
return { handled: true, tools: result.tools };
};
try {
Expand Down
6 changes: 3 additions & 3 deletions src/lib/server/a2a-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ import {
getSdkServer,
isToolAvailableForVersion,
listRegisteredToolNames,
resolveDiscoveryVersion,
type AdcpAuthInfo,
type AdcpServer,
} from './adcp-server';
Expand Down Expand Up @@ -810,9 +811,8 @@ function buildAgentCard(server: AdcpServer, overrides: A2AAgentCardOverrides): A
throw new Error('createA2AAdapter: only the JSONRPC A2A transport is supported');
}
const registeredTools = listRegisteredTools(server);
const tools = registeredTools.filter(toolName =>
isToolAvailableForVersion(server, toolName, server.getAdcpVersion())
);
const discoveryVersion = resolveDiscoveryVersion(server);
const tools = registeredTools.filter(toolName => isToolAvailableForVersion(server, toolName, discoveryVersion));
const availableTools = new Set(tools);
const registeredToolSet = new Set(registeredTools);
const skills = filterPublicAgentCardSkills(
Expand Down
27 changes: 22 additions & 5 deletions src/lib/server/adcp-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -310,17 +310,27 @@ export interface AdcpServer {
invoke(options: AdcpInvokeOptions): Promise<McpToolResponse>;

/**
* Returns the AdCP protocol version this server is configured to speak.
* Returns the newest AdCP protocol version this server can serve.
*
* Defaults to {@link ADCP_VERSION} (the GA version the SDK ships against)
* unless overridden via `createAdcpServer({ adcpVersion })`. This is the
* protocol version, **not** the publisher's app version (`config.version`).
*
* Plumbing surface — Stage 2 of the multi-version refactor exposes the
* configured value but does not yet vary validator/schema selection by
* configured ceiling but does not yet vary validator/schema selection by
* version. Wire-shape adapters key off this getter in subsequent stages.
*/
getAdcpVersion(): string;

/**
* Returns the AdCP protocol version selected when a caller supplies no
* `adcp_version` or `adcp_major_version` claim.
*
* Defaults to {@link getAdcpVersion} unless the server config declares a
* lower `defaultAdcpVersion`. This lets one endpoint retain a conservative
* wire default while making newer explicitly versioned routes reachable.
*/
getDefaultAdcpVersion(): string;
}

/**
Expand Down Expand Up @@ -446,10 +456,13 @@ export function setDiscoveryVersionResolver(server: AdcpServer, resolver: AdcpDi
});
}

/** Resolve a requested discovery version, falling back to the server pin. @internal */
/** Resolve a requested discovery version, falling back to the server default. @internal */
export function resolveDiscoveryVersion(server: AdcpServer, requestedVersion?: string): string {
const resolver = (server as AdcpServerInternal)[ADCP_DISCOVERY_VERSION_RESOLVER];
return resolver ? resolver(requestedVersion) : server.getAdcpVersion();
// The getter is required on current AdcpServer instances. Keep the runtime
// fallback for mixed dependency graphs that pass a wrapper created by an
// older SDK copy, while the public type exposes the new API as required.
return resolver ? resolver(requestedVersion) : (server.getDefaultAdcpVersion?.() ?? server.getAdcpVersion());
}

/** Attach the resolved static MCP catalog for transport adapters. @internal */
Expand Down Expand Up @@ -723,7 +736,10 @@ export function wrapMcpServer(
inner: McpServer | AdcpServerInternal,
compliance?: AdcpServerComplianceApi,
adcpVersion: string = ADCP_VERSION,
options: { structuredContentTextFallback?: StructuredContentTextFallback } = {}
options: {
structuredContentTextFallback?: StructuredContentTextFallback;
defaultAdcpVersion?: string;
} = {}
): AdcpServerInternal {
if (isAdcpServer(inner)) return inner;
const mcp = inner as McpServer;
Expand Down Expand Up @@ -799,6 +815,7 @@ export function wrapMcpServer(
dispatchTestRequest: dispatch as AdcpServerInternal['dispatchTestRequest'],
invoke,
getAdcpVersion: () => adcpVersion,
getDefaultAdcpVersion: () => options.defaultAdcpVersion ?? adcpVersion,
};
return wrapper;
}
Loading
Loading