Skip to content

feat(server): opt-in tool-schema projection from x-adcp-applies-when (prototype) - #3415

Draft
bokelley wants to merge 4 commits into
mainfrom
bokelley/applies-when-projection
Draft

bokelley wants to merge 4 commits into
mainfrom
bokelley/applies-when-projection

Conversation

@bokelley

@bokelley bokelley commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

Prototype. Draft. Not for merge as-is. This exists so the AdCP working group can review a real consumer of the proposed x-adcp-applies-when annotation alongside adcp#8452 (RFC, revised 2026-10-24), adcp#8454 (annotation + lint), and adcp#8458 (media_buy.supported_delivery_types). It follows the revised Semantics section of docs/reference/schema-extensions.mdx in #8454.

What it does

  • projectSchemaForCapabilities(schema, capabilities), exported @experimental from @adcp/sdk/schemas, is a pure function. It returns the schema without the optional properties whose x-adcp-applies-when doesn't apply to an agent with that get_adcp_capabilities response. appliesWhenSatisfied(annotation, capabilities) is exported alongside it for docs and coverage tooling.
  • createAdcpServer({ projectToolSchemas: true }) is an opt-in, @experimental server option. When it's on, every inputSchema / outputSchema advertised in MCP tools/list is projected against the server's own, server-wide capabilities declaration. That covers both the legacy MCP tools/list wrapper and the 2026-07-28 adapter.
  • Off by default. With the option off, no projector is attached and the code path is the same as on main. Tests assert that tools/list is byte-identical on both transports and both tool profiles.

Semantics implemented

Rule Behavior
delivery_types matched against media_buy.supported_delivery_types; an undeclared (or empty, hence invalid) list matches
pricing_models matched against media_buy.supported_pricing_models; an undeclared (or empty) list matches
capabilities dotted path into the capabilities response; matches when present, not null, and not false. Absent doesn't match
No root block when the agent publishes no block at the key's root (e.g. no media_buy on a governance agent), the key matches
Combining OR across every listed value in every key; a key this SDK doesn't know, or a malformed annotation, keeps the property
Dropping only deletes from properties. required, additionalProperties, enum values, and $defs entries are untouched
Required anywhere never dropped: parent required, allOf/anyOf/oneOf arms and if/then/else (recursively, so names only in if count), draft-07 dependencies (both forms), dependentRequired, dependentSchemas. Mirrors the lint's requiredNames
Closed objects nothing is dropped from an object with additionalProperties: false (or unevaluatedProperties: false)
Nesting a dropped property takes its subtree, so a property survives only if it and every ancestor match

The last three guards are defensive. The spec forbids those placements, and the #8454 lint rejects them.

Traversal: properties, items/prefixItems, allOf/anyOf/oneOf, if/then/else, not, additionalProperties, patternProperties, dependentSchemas, and $defs/definitions. Instance data (const, default, enum, examples) and x-* values are left alone.

$refs: The schemas tools/list advertises are not dereferenced. They're the self-contained MCP manifest bundles: one document, with refs like <doc $id>#/$defs/external:core~1product.json. The annotation is read from the property declaration before any dereferencing, as the spec now requires. The function projects the whole document, $defs included, and leaves each ref pointing at its now-projected target, so a $ref'd schema projects the same way in every context. That matches "keys are seller-global".

Purity: The input is never mutated (tested on a deep-frozen fixture under 'use strict'). Unchanged subtrees are shared by reference, and the input itself comes back when nothing is removed. Projecting all 156 bundled 3.2.6 MCP schemas (about 20 MB) takes about 135 ms in total, and all of them are returned unchanged.

Try it

const server = createAdcpServer({
  // ...
  projectToolSchemas: true,
  capabilities: {
    // 3.2 types don't have the field yet; overrides are deep-merged into get_adcp_capabilities.
    overrides: { media_buy: { supported_delivery_types: ['guaranteed'] } as any },
  },
});
// tools/list now omits properties annotated { delivery_types: ['non_guaranteed'] },
// e.g. pricing-option floor_price / price_guidance once the bundled schemas carry #8454's tags.

Or call it directly: projectSchemaForCapabilities(getMcpToolSchema('get_products', 'output'), capabilities).

What it doesn't do (yet)

  • Accepted records aren't exempted. The spec says projections SHOULD NOT be applied to schemas of records the buyer already accepted (committed proposal or media-buy snapshots). This prototype projects every advertised schema it's given. It's documented in the JSDoc and the docs note as not handled.
  • The bundled AdCP schemas (3.2.6) carry no annotations, so today the option only affects custom tools whose schemas carry them. Zod 4 .meta({ 'x-adcp-applies-when': ... }) works, and the integration test uses it. A real effect on AdCP tools waits for a 3.3 bundle with #8454.
  • The default legacy tools/list advertises passthrough { type: 'object' } for most tools (outside the 3.2 media-buy profile, or with exposeToolSchemas). There's nothing to project there.
  • Output schemas of framework tools aren't advertised today. With canonical-pricing-option no longer annotated (it's closed), every tagged property in the revised set (pricing-option floor_price / price_guidance, product.measurement_readiness / vendor_metric_optimization / trusted_match) lives mainly in response schemas tools/list doesn't send.
  • No memoization. Each tools/list call re-projects.
  • Call-time validation, A2A, and get_adcp_capabilities are untouched. That matches the spec's receiver rule: receivers read the canonical schema, so a projected-away field is still accepted.

Resolved by the RFC revision

My first round of questions, and how the revised #8452 / #8454 answers them (all now implemented here):

  1. Shared $defs across contexts / check_governance. Keys are seller-global, so a $ref'd schema projects the same way everywhere. The new root-block rule covers the governance case: a governance agent publishes no media_buy, so media_buy.* keys match and an embedded product keeps every field. Also, canonical-pricing-option is no longer annotated.
  2. Absent capabilities path: doesn't match (drop), because capability blocks declare support by being present. That's what this already did, now with the root-block exception.
  3. Empty supported_delivery_types: []: treated as undeclared. It's invalid anyway (minItems: 1).
  4. Conditional requirements: an annotated property can't be required anywhere in its parent. The projection skips any such name defensively, mirroring the lint.
  5. Closed objects: annotating there is forbidden. The projection never drops from a closed object.
  6. Which capabilities view: the agent's own, server-wide get_adcp_capabilities.
  7. Enum values: projections never remove them. This removes only properties.

Still open

  1. Accepted records have no machine-readable marker. "SHOULD NOT project schemas of accepted records" can't be honored automatically unless a schema says it describes one, for example an x-adcp-* marker on the committed-proposal and media-buy snapshot schemas. Should #8454 add one, or leave it to implementers' per-tool judgment?
  2. Bundler preservation. "Read before dereferencing" needs the annotation to survive as a sibling of $ref in the published MCP manifest bundles. Today's bundles keep x-status / x-added-in siblings, so it likely works, but should the bundler contract say so explicitly?
  3. Union-arm rule enforcement. "An annotated property MUST NOT take part in selecting an arm" isn't in the lint's list of checks, and a projection can't cheaply detect it. Is it lint-enforced, or documentary?
  4. Small edges. This treats a capability path with a single segment as its own root (absent means no match). It also guards unevaluatedProperties: false like additionalProperties: false, which the spec doesn't mention. Worth a sentence in the spec?

Checks run

  • npm run typecheck: pass
  • npm run build:lib: pass
  • npx prettier --check on changed files: pass (repo-wide format:check in the pre-push hook: pass)
  • npx eslint on new and changed TS: no new findings. adcp-server.ts has a pre-existing no-require-imports error that's also on main, and it's outside npm run lint's scope.
  • npm run lint:boundaries: pass
  • npm run ci:doc-links: pass
  • npm run ci:docs-check: no drift
  • test/lib/applies-when-projection.test.js: 28/28 pass (unit fixtures + integration through legacy dispatchTestRequest and a real 2026-07-28 MCP client)
  • Related suites (mcp-modern-negotiation, public-barrel-exports, proposal-negotiation-server, media-buy-lifecycle-compatibility, versioned-tool-schemas) plus the new file: 86/86 pass on this revision
  • npm run test:lib:fast (first revision; this revision changes only the projection module, its tests, JSDoc, and docs): 18,672 pass; 21 fail and 123 cancelled, all environmental. The sandbox exports DATABASE_URL=postgresql://…@localhost:58433/… with no Postgres running, so every Postgres-backed suite fails with ECONNREFUSED. Rerunning those 11 files with DATABASE_URL unset gives 0 failures. CI's Postgres service should cover them.

🤖 Generated with Claude Code

…(prototype)

Add projectSchemaForCapabilities(schema, capabilities) to @adcp/sdk/schemas
and an experimental, off-by-default projectToolSchemas server option. When
on, MCP tools/list input and output schemas (legacy and 2026-07-28
transports) drop optional properties whose proposed x-adcp-applies-when
annotation doesn't match the server's own get_adcp_capabilities declaration.

Prototype for adcontextprotocol/adcp#8452 / #8454. Bundled AdCP schemas don't
carry the annotation yet; tests use fixture schemas shaped like the bundled
MCP manifests ($id-absolute refs into $defs).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
bokelley added a commit to adcontextprotocol/adcp that referenced this pull request Oct 24, 2026
…bilities

The adcp-client prototype (adcontextprotocol/adcp-client#3415) found that
shared schemas reach other agents' tools: a governance agent's
check_governance request embeds products and pricing options, but that agent
publishes no media_buy block. A key whose root capability block the agent
doesn't publish now matches, so the property is kept.

Refs #8452.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
bokelley and others added 2 commits October 24, 2026 22:37
Follow the 2026-10-24 revision of adcontextprotocol/adcp#8452 / #8454:

- a key matches when the agent publishes no block at its root (e.g. no
  media_buy on a governance agent);
- never drop from closed objects (additionalProperties /
  unevaluatedProperties false);
- treat a name required anywhere in the parent as protected, recursively
  through allOf/anyOf/oneOf and if/then/else (including if-only names) and
  draft-07 dependencies, mirroring the spec lint;
- document that schemas of accepted records aren't exempted yet.

Fixtures follow the revised annotation set (cancellation_policy and
conversion_tracking untagged; measurement_readiness tagged).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The opt-in x-adcp-applies-when projection pushes the packed tarball to
20,576,520 bytes in CI, 1,520 bytes over the 20,575,000 budget. Raise it to
20,610,000 to leave about 33 KB of compressor variance, following the
existing budget-comment convention. Other budgets are unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@bokelley bokelley closed this Oct 26, 2026
@bokelley bokelley reopened this Oct 26, 2026
…-projection

# Conflicts:
#	scripts/check-package-size.mjs

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant