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/add-capability-selected-runtime-projections.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"adcontextprotocol": minor
---

Define deterministic capability-selected runtime tool projections, publish concise manifest summaries for every active Media Buy and Creative role tool, and keep response schemas available for lazy SDK validation outside model context.
59 changes: 58 additions & 1 deletion docs/building/by-layer/L0/schemas.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -302,7 +302,64 @@ parent response schemas and task-result resolution metadata out of band.
Structural schemas deliberately omit descriptions. To support tool selection,
clients combine the model-context inputs with each live MCP tool's concise
`name` and `description`; the downloadable model-context manifest is not a
description catalog by itself.
description catalog by itself. Every tool in the published active Media Buy
and Creative role catalogs carries a concise manifest `summary` that a host can
use as that live description.

#### Capability-selected runtime projection

AdCP 3.2 hosts MUST derive a live MCP tool surface from the release manifest
and the tools the endpoint can actually dispatch. A runtime projection is a
selection of the generated per-tool bundles, not another schema profile and
not a second hand-maintained tool catalog.

The deterministic selection algorithm is:

1. Build `implemented_tools` from the endpoint's dispatch registry. Every name
MUST exist in the canonical release manifest. Do not infer implementation
from documentation, a role profile, or a protocol claim.
2. If the session has no narrower capability scope, select
`implemented_tools`. Otherwise, select the implemented tools whose manifest
`protocol` is enabled, unioned with exact enabled tool names. Convert
`supported_protocols` snake case to manifest kebab case (`media_buy` →
`media-buy`) before comparing. Exact tool claims that are not implemented
are configuration errors; hosts MUST fail closed rather than advertise
them.
3. Treat `protocol` as ownership metadata, not dependency closure. Shared task,
account, or discovery tools are included only when the host adds their exact
names. Selection never pulls in neighboring tools implicitly.
4. Production projections MUST exclude the `compliance` protocol. Deprecated
compatibility facades are included only when the endpoint really implements
and advertises them; deprecation alone is not a runtime filter.
5. Sort selected names lexicographically. For each selected name, emit one MCP
`tools/list` entry containing `name`, the optional manifest `summary` as the
live `description`, and the corresponding self-contained `inputSchema` from
the MCP projection. Do not emit unselected tools or load response schemas
into the model-facing list.
6. Keep the release manifest and response bundles available outside model
context. SDKs validate a direct result through that tool's `response_schema`;
they resolve terminal polling results through `task_result_resolution`.

The coarse protocol set normally comes from `supported_protocols`. Exact names
come from the active capability blocks (for example `lifecycle_tools`,
`repair_tasks`, and `projection_tasks`) plus shared tools the host exposes for
that session. The implementation registry remains the upper bound in every
case. Unknown protocol or tool names, duplicate selector inputs, and production
attempts to expose compliance tools are errors.

For caller-side version adaptation, the live discovery result is authoritative.
An SDK calls a current split 3.2 tool when that name is present. If it is absent,
the SDK checks the current tool's `legacy_fallback` manifest entry and may use
the named legacy tool only when that name is live: `direct` is a one-call
translation, `orchestrated` must preserve the current operation's state and
idempotency semantics across the sequence, and `none` is unsupported. SDKs
MUST NOT infer a fallback from a shared protocol classification.

The checked-in `production`, `media-buy`, and `creative` profiles remain useful
catalogs and role-oriented starting points. They are not mandatory runtime
combinations. The complete canonical projection remains the authority for
documentation, conformance, compatibility analysis, code generation, and lazy
response validation.

AdCP 4.0 will make JSON Schema 2020-12 the canonical source dialect. That
major-version migration is where the protocol may selectively use
Expand Down
2 changes: 1 addition & 1 deletion docs/protocol/calling-an-agent.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ The agent-facing version of this content lives at [`skills/call-adcp-agent/SKILL

Walk these in order on first contact with any new agent:

1. **Agent card** (A2A) or **`tools/list`** (MCP): returns tool *names*. AdCP MCP servers no longer publish per-tool parameter schemas in `tools/list` — every tool shows `{type: 'object', properties: {}}`. Don't try to infer shape from there.
1. **Agent card** (A2A) or **`tools/list`** (MCP): returns the tools selected for this endpoint or session. An AdCP 3.2 MCP server includes each selected tool's self-contained `inputSchema`; it does not load the complete AdCP catalog or its response schemas into model context. Treat the live names as the authority for what can be called.
2. **[`get_adcp_capabilities`](/docs/protocol/get_adcp_capabilities)**: returns supported protocols, AdCP major versions, and feature flags. Tells you *which* tools this agent supports, not how to call them. See [`get_adcp_capabilities`](/docs/protocol/get_adcp_capabilities).
3. **`get_schema(tool_name)`** *(when the agent exposes it — pending standardization, see [#3057](https://github.com/adcontextprotocol/adcp/issues/3057))*: returns the JSON Schema for a specific tool's request/response.
4. **Bundled schemas** (offline, authoritative): every published AdCP version ships JSON Schemas for every tool, signed via Sigstore. The path differs by SDK — the spec repo source uses `dist/schemas/<version>/bundled/`, `@adcp/sdk` puts them at `schemas/cache/<version>/bundled/` after `npm run sync-schemas`, Python and Go SDKs use their own conventions. Don't hardcode a path; let the SDK's loader find them. Once located, each schema lives at `<protocol>/<tool>-{request,response}.json`.
Expand Down
14 changes: 14 additions & 0 deletions scripts/build-schemas.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -1180,6 +1180,18 @@ function discoverTools(sourceDir) {
const requestSchema = JSON.parse(fs.readFileSync(requestPath, 'utf8'));
const responseName = `${toolBase}-response.json`;
const responseSchema = `${protocol}/${responseName}`;
const summary = requestSchema['x-tool-summary'];
if (summary !== undefined && (
typeof summary !== 'string'
|| summary.length === 0
|| summary.length > 240
|| summary !== summary.trim()
|| /[\r\n]/.test(summary)
)) {
throw new Error(
`Manifest generation: ${protocol}/${f.name} has invalid x-tool-summary metadata`
);
}
const legacyFallback = requestSchema['x-legacy-fallback'];
if (legacyFallback !== undefined) {
const keys = legacyFallback && typeof legacyFallback === 'object' && !Array.isArray(legacyFallback)
Expand Down Expand Up @@ -1223,6 +1235,7 @@ function discoverTools(sourceDir) {
name: toolName,
protocol,
mutating,
...(summary ? { summary } : {}),
operation_family: requestSchema['x-operation-family'] || toolName,
idempotency_requirement: Array.isArray(requestSchema.required) && requestSchema.required.includes('idempotency_key')
? 'required'
Expand Down Expand Up @@ -1305,6 +1318,7 @@ function buildManifest(sourceDir, urlVersion, semverVersion, repoRoot) {
toolsObj[t.name] = {
protocol: t.protocol,
mutating: t.mutating,
...(t.summary ? { summary: t.summary } : {}),
operation_family: t.operation_family,
idempotency_requirement: t.idempotency_requirement,
request_schema: t.request_schema,
Expand Down
108 changes: 107 additions & 1 deletion scripts/mcp-schema-projection.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const PRESENTATION_ANNOTATIONS = new Set([
'enumDescriptions',
'examples',
'title',
'x-tool-summary',
]);

// Model prompt views communicate request shape while the parent role profile
Expand Down Expand Up @@ -617,6 +618,106 @@ function writeJson(filename, value) {
fs.writeFileSync(filename, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
}

function uniqueStringSet(values, label, { required = false } = {}) {
if (values === undefined && !required) return new Set();
if (!Array.isArray(values) || values.some(value => typeof value !== 'string' || value.length === 0)) {
throw new Error(`${label} must be an array of non-empty strings`);
}
const result = new Set(values);
if (result.size !== values.length) throw new Error(`${label} must not contain duplicates`);
return result;
}

/**
* Select the exact runtime tool surface from a release manifest.
*
* implementedTools is the host's dispatch registry and is always the hard
* upper bound. capabilityProtocols is a coarse protocol-family scope;
* capabilityTools adds exact cross-protocol tools and per-session exceptions.
* Protocol classification is ownership metadata, not dependency closure, so
* this function never adds related tools implicitly.
*/
function selectRuntimeToolNames(manifest, {
implementedTools,
capabilityProtocols,
capabilityTools,
production = true,
} = {}) {
if (!manifest || typeof manifest !== 'object' || !manifest.tools || typeof manifest.tools !== 'object') {
throw new Error('manifest must contain a tools object');
}

const implemented = uniqueStringSet(implementedTools, 'implementedTools', { required: true });
const protocolInputs = uniqueStringSet(capabilityProtocols, 'capabilityProtocols');
const protocols = new Set([...protocolInputs].map(protocol => protocol.replaceAll('_', '-')));
if (protocols.size !== protocolInputs.size) {
throw new Error('capabilityProtocols must not contain equivalent snake_case and kebab-case values');
}
const exactTools = uniqueStringSet(capabilityTools, 'capabilityTools');
const knownTools = new Set(Object.keys(manifest.tools));
const knownProtocols = new Set(Object.values(manifest.tools).map(tool => tool.protocol));

for (const toolName of implemented) {
if (!knownTools.has(toolName)) throw new Error(`implementedTools names unknown tool ${toolName}`);
}
for (const protocol of protocols) {
if (!knownProtocols.has(protocol)) throw new Error(`capabilityProtocols names unknown protocol ${protocol}`);
if (production && protocol === 'compliance') {
throw new Error('production runtime projections cannot select the compliance protocol');
}
}
for (const toolName of exactTools) {
if (!knownTools.has(toolName)) throw new Error(`capabilityTools names unknown tool ${toolName}`);
if (!implemented.has(toolName)) {
throw new Error(`capabilityTools advertises unimplemented tool ${toolName}`);
}
if (production && manifest.tools[toolName].protocol === 'compliance') {
throw new Error(`production runtime projections cannot select compliance tool ${toolName}`);
}
}

const hasCapabilityScope = capabilityProtocols !== undefined || capabilityTools !== undefined;
return [...implemented]
.filter(toolName => !production || manifest.tools[toolName].protocol !== 'compliance')
.filter(toolName => (
!hasCapabilityScope
|| exactTools.has(toolName)
|| protocols.has(manifest.tools[toolName].protocol)
))
.sort();
}

/** Build MCP tools/list entries from already-generated per-tool bundles. */
function buildRuntimeToolsList(projectionManifest, selectedToolNames, loadInputSchema) {
if (
!projectionManifest
|| typeof projectionManifest !== 'object'
|| !projectionManifest.tools
|| typeof projectionManifest.tools !== 'object'
) {
throw new Error('projectionManifest must contain a tools object');
}
const selected = uniqueStringSet(selectedToolNames, 'selectedToolNames', { required: true });
if (typeof loadInputSchema !== 'function') throw new Error('loadInputSchema must be a function');

return [...selected].sort().map(name => {
const tool = projectionManifest.tools[name];
if (!tool) throw new Error(`selectedToolNames names unknown projected tool ${name}`);
if (typeof tool.inputSchema !== 'string') {
throw new Error(`projected tool ${name} does not provide inputSchema`);
}
const inputSchema = loadInputSchema(tool.inputSchema, name);
if (!inputSchema || typeof inputSchema !== 'object' || Array.isArray(inputSchema)) {
throw new Error(`loadInputSchema returned an invalid schema for ${name}`);
}
return {
name,
...(tool.summary ? { description: tool.summary } : {}),
inputSchema: clone(inputSchema),
};
});
}

function generateMcpSchemaProjection({
sourceDir,
targetDir,
Expand Down Expand Up @@ -680,7 +781,10 @@ function generateMcpSchemaProjection({

for (const [toolName, tool] of Object.entries(manifest.tools || {})) {
if (!toolFilter(toolName, tool)) continue;
const projectedTool = { protocol: tool.protocol };
const projectedTool = {
protocol: tool.protocol,
...(tool.summary ? { summary: tool.summary } : {}),
};
for (const [field, relativePath] of [
['inputSchema', tool.request_schema],
['outputSchema', tool.response_schema],
Expand Down Expand Up @@ -739,13 +843,15 @@ module.exports = {
MCP_PROTOCOL_VERSION,
assertDraft07SourceSchema,
assertLocalRefsResolve,
buildRuntimeToolsList,
collectExternalRefs,
compactDraft07Schema,
enforceSchemaBounds,
generateMcpSchemaProjection,
measureSchema,
projectDraft07Node,
projectSourceSchema,
selectRuntimeToolNames,
stripModelContextAnnotations,
stripPresentationAnnotations,
};
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"$id": "/schemas/account/get-account-financials-request.json",
"title": "Get Account Financials Request",
"description": "Request financial status for an operator-billed account. Returns spend summary, credit/balance status, and invoice history. Only applicable when the seller declares account_financials capability.",
"x-tool-summary": "Retrieve spend, credit or balance status, and invoice history for an operator-billed account.",
"type": "object",
"allOf": [
{
Expand Down
1 change: 1 addition & 0 deletions static/schemas/source/account/list-accounts-request.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"$id": "/schemas/account/list-accounts-request.json",
"title": "List Accounts Request",
"description": "Request parameters for listing accounts accessible to the authenticated agent. For upstream-managed namespaces, this discovers seller/storefront account_id values. For buyer-declared accounts, responses return the natural-key fields needed to reconstruct an AccountRef after a cold start.",
"x-tool-summary": "List accounts accessible to the caller and recover their canonical account references.",
"type": "object",
"allOf": [
{
Expand Down
1 change: 1 addition & 0 deletions static/schemas/source/account/report-usage-request.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"$id": "/schemas/account/report-usage-request.json",
"title": "Report Usage Request",
"description": "Reports how a vendor's service was consumed after campaign delivery. Used by orchestrators (DSPs, storefronts) to inform vendor agents (signals, governance, creative, or — when the receiving agent is the seller of the media buy itself — a sales agent for buyer-attested or vendor-attested billing reconciliation) what was used so the receiver can track earned revenue and verify billing. Records can span multiple accounts and campaigns in a single request. When the buy's `measurement_terms.billing_measurement.vendor` names a party other than the seller's own ad server, that party (or the buyer on its behalf) pushes final measurements via this task; the seller invoices against those final records. See [Billing authority](/docs/media-buy/advanced-topics/billing-authority) for the end-to-end flow.",
"x-tool-summary": "Report vendor-service consumption and final measurements for revenue tracking and billing reconciliation.",
"type": "object",
"allOf": [
{
Expand Down
1 change: 1 addition & 0 deletions static/schemas/source/account/sync-accounts-request.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"$id": "/schemas/account/sync-accounts-request.json",
"title": "Sync Accounts Request",
"description": "Sync advertiser account state with a seller. Two modes, distinguished by the key on each per-account entry:\n\n- **Provisioning mode** (`brand` + `operator` + `billing` at the entry root): the agent declares the advertiser identity, operator, optional operator-owned buying unit, optional fixed account currency, sandbox disposition, and billing model. The seller provisions or links the corresponding advertiser object via upsert. `brand.countries`, `operator_unit.id`, `currency`, and `sandbox` participate in the buyer-declared natural key when present; `operator_unit.name` is display metadata only. The seller MAY echo a seller-assigned account_id but MUST continue accepting the complete natural-key AccountRef.\n\n- **Settings-update mode** (`account` field carrying an [`AccountRef`](/schemas/core/account-ref.json)): targets an existing account by seller/storefront `account_id` or buyer-declared natural key. The seller updates settable state without provisioning side effects.\n\nExactly one key shape is allowed per entry. Sellers that do not implement one mode return `UNSUPPORTED_PROVISIONING` for that mode.",
"x-tool-summary": "Provision advertiser accounts or update settings for existing accounts through declarative synchronization.",
"type": "object",
"allOf": [
{
Expand Down
1 change: 1 addition & 0 deletions static/schemas/source/account/sync-governance-request.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"$id": "/schemas/account/sync-governance-request.json",
"title": "Sync Governance Request",
"description": "Sync the governance agent endpoint against specific accounts. The service persists the governance agent and calls it for approval during governed lifecycle events via check_governance. Uses replace semantics: each call replaces any previously synced agent on the specified accounts. The service MUST verify that the authenticated agent has authority over each referenced account before persisting the governance agent.\n\nThe binding is **account-scoped, not plan-scoped**. Each account binds to exactly one governance agent, and that agent owns the lifecycle for every plan on the account. Initial buyer-side checks address a plan by plan_id; downstream services pass only the opaque governance_context, from which the issuing agent recovers the plan. Governance registration does not vary per plan.\n\nA plan is unitary — budget authority, delivery monitoring, and regulatory compliance are phases of the same evaluation (`purchase` / `modification` / `delivery` on check_governance), not specialisms held by different agents — so a single agent owns the full lifecycle. Buyers that need internal specialist review compose that inside the governance agent, not at the registration layer. `governance_agents` is an array (not a scalar) because that is the shape 3.0 shipped with and existing senders MUST continue to work; the `maxItems: 1` constraint is load-bearing. The single-agent rule is also baked into the singular governance_context carried across tasks.",
"x-tool-summary": "Bind or replace the governance agent used for approval across specific advertiser accounts.",
"type": "object",
"allOf": [
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"$id": "/schemas/creative/get-creative-delivery-request.json",
"title": "Get Creative Delivery Request",
"description": "Request parameters for retrieving creative delivery data including variant-level metrics from a creative agent. At least one scoping filter (media_buy_ids or creative_ids) is required.",
"x-tool-summary": "Retrieve creative and variant-level delivery metrics scoped to media buys or creative IDs.",
"type": "object",
"allOf": [
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"$id": "/schemas/creative/get-creative-features-request.json",
"title": "Get Creative Features Request",
"description": "Request payload for the get_creative_features task. Submits a creative manifest for evaluation by a governance agent, which analyzes the creative and returns scored feature values (brand safety, content categorization, quality metrics, etc.).",
"x-tool-summary": "Evaluate a creative manifest and return scored safety, content, quality, and governance features.",
"type": "object",
"allOf": [
{
Expand Down
Loading
Loading