spec(versioning): release-precision protocol version negotiation#3493
Merged
spec(versioning): release-precision protocol version negotiation#3493
Conversation
Promote AdCP version to a top-level envelope field on every request and response. Clients send their pin; servers echo what they served. Minor precision on the wire; patches surface as build_version operational metadata. Transport-uniform across MCP and A2A.
…lope field Implements the RFC at specs/version-negotiation.md. - Add `adcp_version` (release-precision semver string) to all 63 request schemas (alongside existing `adcp_major_version`, marked deprecated) and all 63 response schemas (new — server echoes what it served). - Add `supported_versions` and `build_version` to capabilities response. Mark `adcp.major_versions` as deprecated. - Add `error-details/version-unsupported.json` standardizing the `VERSION_UNSUPPORTED` error data payload. - Update docs/reference/versioning.mdx to document bidirectional release-precision negotiation, deprecation of major-precision fields, and patch-not-negotiated rule. - Changeset: minor bump. Fully additive on the wire. Legacy fields removed in 4.0.
4 tasks
bokelley
added a commit
to adcontextprotocol/adcp-client-python
that referenced
this pull request
Apr 29, 2026
Lifts the per-instance adcp_version pin from plumbing-only (Stage 2) to actual wire emission. Builds against the upstream RFC at adcontextprotocol/adcp#3493 — assumes that lands. Client side: - ProtocolAdapter gains envelope_enricher hook + _enrich_outgoing_params helper. Hook runs after idempotency injection, before validation. - MCPAdapter._call_mcp_tool and A2AAdapter._call_a2a_tool both apply the enricher to the outbound params dict. - ADCPClient.__init__ installs an enricher that prepends adcp_version=self._adcp_version to every outbound request. Caller- supplied values on the params dict win over the pin (per-call override remains available once generated request types declare the field). Server side: - capabilities_response() accepts adcp_version, supported_versions, build_version. Emits supported_versions (release-precision list) and build_version (advisory full semver) on the adcp block, plus top-level adcp_version on the response envelope. Legacy major_versions still emitted for back-compat through 3.x. - ADCPServerBuilder's auto-generated get_adcp_capabilities handler threads the builder's pinned adcp_version into capabilities_response(). 10 new tests in tests/test_adcp_version_wire.py cover envelope injection, default-value behavior, caller override, capability response shape, and the auto-capabilities handler.
Post-triage fixes from external review + internal team feedback:
* Extract `adcp_version` and `adcp_major_version` to shared
`core/version-envelope.json`; compose into all 127 task schemas via
`allOf $ref`. Single source of truth replaces 127 inline copies of
the same description, regex, and examples.
* Revert `core/pagination-response.json` field pollution. That schema
is a fragment composed via `pagination: { $ref: ... }` and is
`additionalProperties: false`, so adding `adcp_version` there created
a meaningless validated location at `response.pagination.adcp_version`.
* Resolution algorithm: same-major-but-no-release ≤ buyer's pin now
returns `VERSION_UNSUPPORTED` instead of asking the seller to
downshift below its supported window. Sellers do not maintain
validators they don't ship.
* `build_version` format pinned to a full semver string with optional
pre-release and build-metadata segments (`3.1.2+vendor.deploy.42`)
per semver §9–§10. Pattern updated on `error-details/version-unsupported.json`
and `protocol/get-adcp-capabilities-response.json`.
* Pre-release pin semantics specified: `"3.1-beta"` matches exactly,
no range resolution; servers MUST NOT downshift `"3.1"` onto a
pre-release.
* Dual-emit MUST during 3.x: buyers that emit `adcp_version` MUST
also emit `adcp_major_version` (mirror), and majors disagreeing
between the two fields returns `VERSION_UNSUPPORTED`. Lets legacy
3.x sellers keep negotiating off the integer.
* Server response semantics: echoed `adcp_version` is the **release
served**, never the seller's own latest release. A 3.1 seller
serving a 3.0 buyer at 3.0 echoes "3.0".
* MCP `protocolVersion` (initialize handshake) vs AdCP `adcp_version`
(per-call payload) layering: documented as separate concerns in both
the RFC and the user-facing doc. A2A has no equivalent to MCP
initialize, which is the reason `adcp_version` rides on the payload.
* Migration cadence tightened to 3.1 SHOULD on both sides → 3.2 MUST
with compliance grader gating non-echo. Buyer-side pinning is only
useful once sellers actually echo; leaving response echo at
RECOMMENDED for two minors stalls the migration.
* `VERSION_UNSUPPORTED` error code description updated to point at
`error-details/version-unsupported.json` and reference release-precision.
* SDK guidance: `VERSION_UNSUPPORTED` SHOULD raise a typed error, not
silently auto-retry. Auto-downshift is opt-in, not default.
Net diff vs origin/main is now -488 lines smaller than the prior
inline-duplication shape (3728/-514 → 3240/-1041).
All schema validation tests pass (test:schemas, test:examples,
test:json-schema, test:composed, test:extensions).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ompliance Pull back from MUST in 3.2 to SHOULD on both sides through all of 3.x. Reasoning: * The 3.x stability guarantee says fields don't graduate optional → required within a major. A spec MUST in 3.2 for a field that didn't exist in 3.0 dents that guarantee — a 3.0-built implementation shouldn't become non-conformant when 3.2 ships. * The internal team's intuition — "buyers need echo to key off, leaving it RECOMMENDED stalls migration" — is correct. The right lever is the AdCP compliance grader (advisory in 3.1, blocking in 3.2 for sellers that don't echo). Sellers that want certification ship the echo; the spec stays stable. * Dual-emit pulled back to SHOULD too. It's belt-and-suspenders for the transitional period, not a correctness requirement. Disagreement between adcp_version and adcp_major_version still MUST return VERSION_UNSUPPORTED (correctness — if you read both they have to agree). What stays MUST: * "Honor adcp_version when present" — semantic correctness for sellers that read the field. Sellers that don't read it are unaffected. * "Don't drop legacy fields early" — backward-compat through 3.x. * "Cross-major returns VERSION_UNSUPPORTED" — correctness. * "Disagreeing fields return VERSION_UNSUPPORTED" — correctness. Migration table updated in both specs/version-negotiation.md and docs/reference/versioning.mdx; changeset reworded. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This was referenced Apr 29, 2026
…legacy-3.0 echo fallback, in-house breadcrumb Three doc-only fixes from the second expert pass: * Resolution table gains a row covering server `["3.1-beta", "3.1"]` + buyer pin `"3.1"` → exact match wins, never silent downshift to pre-release. Worked example for the "release vs pre-release in the same MAJOR.MINOR" corner. * `versioning.mdx` adds explicit guidance for legacy 3.0 sellers that don't echo `adcp_version`: buyers SHOULD validate against their pin and treat the seller as 3.0-only for capability inference. Closes the gap where the SDK fallback path was implicit. * In-house client breadcrumb at the top of §Bidirectional negotiation so cold readers don't trip on "is this required for me?" before reaching the migration table 60 lines later. * "Released-precision" → "Release-precision" typo fix. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…hemas; add CI lint
The allOf $ref envelope-composition pattern requires permissive
additionalProperties at root. Draft-07 allOf does NOT bypass the
parent schema's additionalProperties — a strict parent rejects the
envelope's fields outright, even though they're declared inside the
$ref'd schema.
17 request schemas under collection/, governance/, property/, and
tmp/ were declaring additionalProperties: false at root. The previous
allOf refactor silently broke those 17 — adcp_version AND legacy
adcp_major_version (which had been inlined before the refactor) were
both rejected. The schema-validation tests didn't catch it because
example payloads don't include the version fields.
Reproduced with Ajv:
- get-property-list-request.json + payload {list_id, adcp_version: "3.1"}
→ "must NOT have additional properties" (adcp_version)
Fix:
- Flip additionalProperties: false → true on the 17 affected schemas.
- Add CI lint (tests/lint-version-envelope.test.cjs) that fails any
schema with allOf $ref to version-envelope.json + strict root.
- Wire under npm run test:version-envelope; included in the umbrella
`npm test` and the `precommit` step.
- Document the invariant in the RFC §1 ("Composition invariant" note).
- Note the behavior change in the changeset.
Strict request validation across these schemas returns at draft 2019-09
via unevaluatedProperties: false — see #3534 for the migration plan.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… dual-emit timeline
Two clarifications surfaced by the SDK-side implementation review:
* RFC §2 gains a "canonical wire shape" subsection with a valid/invalid
table. Pattern allows MAJOR.MINOR or MAJOR.MINOR-PRERELEASE; no patch
component is ever valid on the wire, even alongside pre-release tags.
SDKs that internally key bundles with full-semver patch strings
(e.g. "3.1.0-beta.1") MUST normalize to release-precision
("3.1-beta.1") before emitting. Internal keying can stay exact;
the wire is release-only by construction.
* RFC §SDK consequences gains a "Dual-emit timeline" subsection:
3.x SDKs always dual-emit; 4.0 SDKs emit only adcp_version.
Decision is determined by which spec major the SDK was built
against, not by detecting the seller. A 3.x SDK pointing at a 4.0
server dual-emits and the 4.0 server ignores the integer (allowed
by additionalProperties: true). A 4.0 SDK pointing at a 3.x server
emits only the string and the 3.x server reads it directly.
* Doc mirror at versioning.mdx §"Patches are not negotiated" — same
wire-shape rule with examples of invalid values.
Items 1 (conflict rule), 2 (capability symmetry), 3 (response echo
SHOULD), 5 (VERSION_UNSUPPORTED data shape), 6 (4.0 sunset) from the
review were already in the spec; this commit closes the two real
prose gaps the SDK review surfaced.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… wire `adcp_version` (release-precision) The schema registry, tarball manifests, compliance index, and the /protocol/ HTTP discovery endpoint all carry a field named `adcp_version` whose value has always been the full semver of the published artifact (e.g. "3.1.0-beta.1"). After this PR introduced the wire `adcp_version` field with release-precision constraint (e.g. "3.1", "3.1-beta"), an SDK author reading the meta-field and emitting it on the wire would now get rejected by the wire pattern. Fix: introduce `published_version` as the preferred name on each meta-object (full semver, unambiguous). Keep `adcp_version` as a legacy alias through 3.x for back-compat with @adcp/client (whose `ComplianceIndex.adcp_version` interface reads it). Both meta-aliases sunset in 4.0. Touched: * static/schemas/source/index.json — emits both fields, note expanded. * scripts/build-schemas.cjs, scripts/build-compliance.cjs, scripts/build-protocol-tarball.cjs — write both fields with comments. * scripts/update-schema-versions.cjs, scripts/verify-version-sync.cjs — read/write `published_version`; treat `adcp_version` as alias. * server/src/schemas-middleware.ts — /protocol/ discovery endpoint emits both fields. * server/src/addie/mcp/member-tools.ts — comment notes the typed `@adcp/client` alias path; switch when upstream type bumps. * docs/building/schemas-and-sdks.mdx — version-discovery curl example uses `published_version` and notes the wire-vs-meta distinction. * specs/version-negotiation.md — adds "adcp_version is overloaded" table covering all three layers (wire / meta / legacy extensions). The wire field, the meta field, and the legacy extension field all share the name `adcp_version` for historical reasons. Documenting the overlap is the safest path; mechanical rename would break the @adcp/client TypeScript surface in a coordinated-release way that isn't worth doing inside this PR. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ct, doc breadcrumbs Final-pass expert review surfaced three real issues plus several smaller fixes. All applied here. Real bugs: * Resolution table gap-case (RFC §3 + versioning.mdx). Server ["3.0", "3.2"] + buyer pin "3.1" fell between rows: row 3's predicate "server's max < pin" is false (3.2 < 3.1 false); row 4's "no server release ≤ pin" is also false (3.0 ≤ 3.1). No row fired. Predicate rewritten to "no exact match, at least one server release ≤ pin" which covers both "max < pin" and gap cases unambiguously. Mirrored in versioning.mdx. * TMP privacy contract regression. tmp/identity-match-request.json and tmp/context-match-request.json describe a privacy boundary enforced by additionalProperties: false. The earlier sweep that fixed the strict-additionalProperties regression flipped these to permissive, breaking the contract — callers could now smuggle arbitrary fields including ext/context lookalikes. Fix: revert these two schemas to additionalProperties: false; remove the envelope allOf $ref; inline adcp_version and adcp_major_version in properties. Verified with Ajv that the schemas now reject rogue ext fields while accepting valid adcp_version. * Governance docstring lie. governance/check-governance-request.json plan_id description claimed "Including `account` is rejected by `additionalProperties: false`" — but the regression-fix flipped root to true. Rewrote to "Governance agents MUST treat any sibling `account` field as a contract violation and reject the request" (agent-enforced rather than schema-enforced). Smaller fixes: * verify-version-sync.cjs now checks BOTH published_version and the legacy adcp_version alias against package.json. Hand-edits that update only one are caught. * member-tools.ts:3690 reads published_version with a typed cast, falling back to the legacy adcp_version alias to keep typecheck green against @adcp/client.ComplianceIndex (which still types the legacy field). Eliminates a future migration TODO. * tests/lint-version-envelope.test.cjs docstring narrowed to document its actual scope (root-level allOf only). Schemas that need strict-mode (e.g. TMP privacy endpoints) intentionally don't compose via allOf and are correctly outside the lint's scope. * version-envelope.json description gains an explicit "NORMALIZATION" sentence: SDKs reading full-semver from bundle metadata MUST normalize to release-precision before emitting on the wire. Surfaces in generated SDK docstrings. * versioning.mdx adds a meta-vs-wire breadcrumb after the wire-shape paragraph, with a link to schemas-and-sdks#version-discovery for the discovery URL. * build-protocol-tarball.cjs comment phrasing aligned with the other build scripts (consistent "@adcp/client.ComplianceIndex" reference). All schema validation tests green: test:schemas (7), test:examples (34), test:composed (32), test:version-envelope (1), verify-version-sync. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This was referenced Apr 29, 2026
bokelley
added a commit
to adcontextprotocol/adcp-client-python
that referenced
this pull request
Apr 29, 2026
…ssion Per the merged AdCP version-negotiation spec (adcontextprotocol/adcp#3493, core/version-envelope.json): "SDKs that read full-semver values from bundle metadata (e.g. ComplianceIndex.published_version = '3.1.0-beta.1') MUST normalize to release-precision ('3.1-beta.1') before emitting on the wire — meta-field values are NOT valid wire values." The packaged ADCP_VERSION file ships full-semver ('3.0.0' today). We were passing it through unchanged to the wire, which is non-compliant. Adds normalize_to_release_precision() to _version.py and applies it in resolve_adcp_version() so: - Patch-precision input ('3.0.0', '3.0.1') → stored/emitted as '3.0' - Release-precision input ('3.0', '3.1-beta') → unchanged - Pre-release tags preserved ('3.1.0-rc.1' → '3.1-rc.1') get_adcp_version() returns the normalized form regardless of what the caller passed — wire values are the canonical form. 13 new normalization tests; existing tests updated to assert normalized output where the input was patch-precision.
bokelley
added a commit
to adcontextprotocol/adcp-client-python
that referenced
this pull request
Apr 30, 2026
Adds Stripe-style per-instance protocol-version pinning. Each ADCPClient / ADCPMultiAgentClient / ADCPServerBuilder accepts an adcp_version constructor option (release-precision string, e.g. "3.0", "3.1", "3.1-beta"). Default = the SDK's compile-time pin (ADCP_VERSION packaged with the wheel). Each surface exposes get_adcp_version(). Stage 2 is plumbing only — the value is stored per-instance and exposed via the getter; no wire emission yet. Stage 3 lifts the cross-major fence and threads the pin through schema/validator selection and outbound wire emission. The protocol RFC for the matching wire field is filed at adcontextprotocol/adcp#3493. Cross-major pins raise ConfigurationError at construction — within major 3 every accepted pin agrees with the wire's ADCP_MAJOR_VERSION constant; no silent drift. Patch-precision strings ("3.0.1") are accepted for backwards compatibility but the spec defines negotiation at release precision only. 44 new tests in tests/test_adcp_version_option.py cover defaults, valid pins, cross-major rejection, unparseable strings, and all four constructor surfaces.
bokelley
added a commit
to adcontextprotocol/adcp-client-python
that referenced
this pull request
Apr 30, 2026
Lifts the per-instance adcp_version pin from plumbing-only (Stage 2) to actual wire emission. Builds against the upstream RFC at adcontextprotocol/adcp#3493 — assumes that lands. Client side: - ProtocolAdapter gains envelope_enricher hook + _enrich_outgoing_params helper. Hook runs after idempotency injection, before validation. - MCPAdapter._call_mcp_tool and A2AAdapter._call_a2a_tool both apply the enricher to the outbound params dict. - ADCPClient.__init__ installs an enricher that prepends adcp_version=self._adcp_version to every outbound request. Caller- supplied values on the params dict win over the pin (per-call override remains available once generated request types declare the field). Server side: - capabilities_response() accepts adcp_version, supported_versions, build_version. Emits supported_versions (release-precision list) and build_version (advisory full semver) on the adcp block, plus top-level adcp_version on the response envelope. Legacy major_versions still emitted for back-compat through 3.x. - ADCPServerBuilder's auto-generated get_adcp_capabilities handler threads the builder's pinned adcp_version into capabilities_response(). 10 new tests in tests/test_adcp_version_wire.py cover envelope injection, default-value behavior, caller override, capability response shape, and the auto-capabilities handler.
bokelley
added a commit
to adcontextprotocol/adcp-client-python
that referenced
this pull request
Apr 30, 2026
…ssion Per the merged AdCP version-negotiation spec (adcontextprotocol/adcp#3493, core/version-envelope.json): "SDKs that read full-semver values from bundle metadata (e.g. ComplianceIndex.published_version = '3.1.0-beta.1') MUST normalize to release-precision ('3.1-beta.1') before emitting on the wire — meta-field values are NOT valid wire values." The packaged ADCP_VERSION file ships full-semver ('3.0.0' today). We were passing it through unchanged to the wire, which is non-compliant. Adds normalize_to_release_precision() to _version.py and applies it in resolve_adcp_version() so: - Patch-precision input ('3.0.0', '3.0.1') → stored/emitted as '3.0' - Release-precision input ('3.0', '3.1-beta') → unchanged - Pre-release tags preserved ('3.1.0-rc.1' → '3.1-rc.1') get_adcp_version() returns the normalized form regardless of what the caller passed — wire values are the canonical form. 13 new normalization tests; existing tests updated to assert normalized output where the input was patch-precision.
bokelley
added a commit
to adcontextprotocol/adcp-client-python
that referenced
this pull request
Apr 30, 2026
…a) (#294) * feat(sdk): per-instance adcp_version constructor option (Stage 2) Adds Stripe-style per-instance protocol-version pinning. Each ADCPClient / ADCPMultiAgentClient / ADCPServerBuilder accepts an adcp_version constructor option (release-precision string, e.g. "3.0", "3.1", "3.1-beta"). Default = the SDK's compile-time pin (ADCP_VERSION packaged with the wheel). Each surface exposes get_adcp_version(). Stage 2 is plumbing only — the value is stored per-instance and exposed via the getter; no wire emission yet. Stage 3 lifts the cross-major fence and threads the pin through schema/validator selection and outbound wire emission. The protocol RFC for the matching wire field is filed at adcontextprotocol/adcp#3493. Cross-major pins raise ConfigurationError at construction — within major 3 every accepted pin agrees with the wire's ADCP_MAJOR_VERSION constant; no silent drift. Patch-precision strings ("3.0.1") are accepted for backwards compatibility but the spec defines negotiation at release precision only. 44 new tests in tests/test_adcp_version_option.py cover defaults, valid pins, cross-major rejection, unparseable strings, and all four constructor surfaces. * feat(sdk): wire emission for adcp_version pin (Stage 3a) Lifts the per-instance adcp_version pin from plumbing-only (Stage 2) to actual wire emission. Builds against the upstream RFC at adcontextprotocol/adcp#3493 — assumes that lands. Client side: - ProtocolAdapter gains envelope_enricher hook + _enrich_outgoing_params helper. Hook runs after idempotency injection, before validation. - MCPAdapter._call_mcp_tool and A2AAdapter._call_a2a_tool both apply the enricher to the outbound params dict. - ADCPClient.__init__ installs an enricher that prepends adcp_version=self._adcp_version to every outbound request. Caller- supplied values on the params dict win over the pin (per-call override remains available once generated request types declare the field). Server side: - capabilities_response() accepts adcp_version, supported_versions, build_version. Emits supported_versions (release-precision list) and build_version (advisory full semver) on the adcp block, plus top-level adcp_version on the response envelope. Legacy major_versions still emitted for back-compat through 3.x. - ADCPServerBuilder's auto-generated get_adcp_capabilities handler threads the builder's pinned adcp_version into capabilities_response(). 10 new tests in tests/test_adcp_version_wire.py cover envelope injection, default-value behavior, caller override, capability response shape, and the auto-capabilities handler. * fix(sdk): normalize adcp_version to release-precision before wire emission Per the merged AdCP version-negotiation spec (adcontextprotocol/adcp#3493, core/version-envelope.json): "SDKs that read full-semver values from bundle metadata (e.g. ComplianceIndex.published_version = '3.1.0-beta.1') MUST normalize to release-precision ('3.1-beta.1') before emitting on the wire — meta-field values are NOT valid wire values." The packaged ADCP_VERSION file ships full-semver ('3.0.0' today). We were passing it through unchanged to the wire, which is non-compliant. Adds normalize_to_release_precision() to _version.py and applies it in resolve_adcp_version() so: - Patch-precision input ('3.0.0', '3.0.1') → stored/emitted as '3.0' - Release-precision input ('3.0', '3.1-beta') → unchanged - Pre-release tags preserved ('3.1.0-rc.1' → '3.1-rc.1') get_adcp_version() returns the normalized form regardless of what the caller passed — wire values are the canonical form. 13 new normalization tests; existing tests updated to assert normalized output where the input was patch-precision. * fix(sdk): code review polish on adcp_version pin Addresses code-reviewer findings on PR #294: - Re-export ConfigurationError from adcp.__init__ so callers can import it from the public package surface (matches every other ADCPError subclass). - Accept SemVer build metadata (3.0.1+canary, 3.1.0-beta+sha.5) on the regex; strip it on wire emission alongside patch. Build metadata is purely a build identifier and never part of a contract. - Document the caller-wins precedence on per-call params dict in ADCPClient.__init__'s adcp_version section. - Drop dead `if TYPE_CHECKING: pass` block in _version.py. Tests: 3 new build-metadata normalization cases. 70/70 passing. * docs(sdk): dx-expert polish on adcp_version pin Addresses dx-expert findings on PR #294: - ConfigurationError docstring trimmed from past-tense narrative to the actionable rule: install the SDK major that targets the wire version you want. Staging history lives in _version.py module docstring (where it belongs). - force_a2a_version docstring gains explicit cross-reference: "Not for AdCP protocol pinning — see adcp_version for that." The two string-shaped version kwargs sit side-by-side; agents skimming the signature will guess wrong without the disambiguation. - adcp_version docstring documents the migration from the legacy adcp_major_version (integer) wire field — both coexist on the wire through 3.x, servers prefer the new field, generated request types still expose the legacy field until tracked schema sync (issue #306) lands. * feat(sdk): per-agent adcp_version map on ADCPMultiAgentClient Addresses adtech-product-expert finding: a holdco trade desk almost always has one seller on a newer release than the others during rollout. Forcing all sub-clients to share a uniform pin makes the multi-client useless the moment one seller ships a new release. ADCPMultiAgentClient now accepts adcp_version: str | dict[str, str] | None: - None → every agent uses the SDK default (unchanged). - str → every agent uses this pin (unchanged). - dict[str, str] → per-agent override map. Agents missing from the map fall back to the SDK default. Each entry is independently validated; cross-major in any entry raises ConfigurationError. multi.get_adcp_version() returns the uniform pin when all agents agree (including the all-same dict case); raises ValueError with the per-agent map in the message when pins are heterogeneous, pointing callers at multi.agent(id).get_adcp_version() for per-agent reads. Also addresses python-expert findings: - responses.py: build_version emit guard tightened from `if x:` to `is not None` for consistency with the rest of the function. - protocols/base.py: envelope_enricher docstring documents the contract that top-level Request models must declare extra="allow" (so the injected adcp_version key passes the post-enrichment schema validator). 6 new tests covering per-agent map, fall-back, heterogeneous get_adcp_version() behavior, and cross-major rejection within a map. 75/75 passing. * chore(types): regen public API snapshot for ConfigurationError Snapshot test caught the new public symbol added in commit ebed151 (re-export ConfigurationError from adcp.__init__). Regenerated via scripts/regenerate_public_api_snapshot.py — addition is intentional.
bokelley
added a commit
to adcontextprotocol/adcp-client
that referenced
this pull request
Apr 30, 2026
) * fix(protocols): caller args win over SDK version envelope (#1072) `ProtocolClient.callTool` was spreading the wire version envelope after caller `args`, silently rewriting any `adcp_major_version` / `adcp_version` the caller put in `args` with the SDK's own pin. This broke the bundled `error-compliance.yaml` `unsupported_major_version` storyboard step (sends `adcp_major_version: 99` to elicit `VERSION_UNSUPPORTED`) and any conformance harness using the SDK as buyer transport to probe seller version negotiation. - Reverse spread order at all four wire-injection sites (in-process MCP, HTTP MCP, A2A, both factory functions) so caller args win. - Add `adcp_version` to `ADCP_ENVELOPE_FIELDS` so the same caller-supplied value survives `SingleAgentClient`'s per-tool schema-strip path (mirrors the existing `adcp_major_version` preservation). - Replace the pseudo-test that asserted spread-order on inline literals with two real regression tests over the in-process MCP path: one for default injection, one for caller override. Stale dual-field drift is still caught at the server boundary by `createAdcpServer`'s field-disagreement check (spec PR `adcontextprotocol/adcp#3493`). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore: sync LIBRARY_VERSION to 5.25.0 Release PR #1068 bumped package.json to 5.25.0 but the auto-generated src/lib/version.ts still reported 5.24.0. The npm-published 5.25.0 SDK consequently mis-reports its own version through `LIBRARY_VERSION` and `VERSION_INFO.library`. Re-running `sync-version.ts` against the current package.json corrects both fields. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor: factor applyVersionEnvelope helper for all 4 wire sites Address expert-review asks on PR #1073: - Single chokepoint (`applyVersionEnvelope`) so a future refactor can't silently flip the spread order on one branch and leave the other three intact (code-reviewer ask). Helper is exported. - New regression tests cover: caller-wins, envelope-fills-gaps, the asymmetric case (caller integer 99 + SDK fills 3.1 string — exercises the server-side disagreement-check path the protocol-expert flagged), caller `adcp_version` override, and the v2 empty-envelope case. - New strip-path test asserts `ADCP_ENVELOPE_FIELDS` contains both `adcp_major_version` and `adcp_version` so a caller-supplied 3.1+ string survives `SingleAgentClient`'s schema-strip path. - Changeset reframed to lead with the 5.24/5.25 → 5.26 behavior change and explicitly cite `createAdcpServer`'s field-disagreement check (5.25) as the reason caller-wins is now safe. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(protocols): mark applyVersionEnvelope @internal The helper is exported for testing but is not intended as a stable public API — `ProtocolClient` and the two factory functions are the only legitimate callers. @internal signals to TypeDoc / api-extractor that consumers should not depend on this surface directly. https://claude.ai/code/session_012cM32L5HpXygCB6qqvYGXd * chore: keep applyVersionEnvelope off the public TypeScript surface The @internal tag (commit 8fdd2b5) drops applyVersionEnvelope from dist/lib/protocols/index.d.ts, but the explicit re-export in src/lib/index.ts pulled it back into dist/lib/index.d.ts as part of the public API. Drop the re-export and have the test import from dist/lib/protocols/index.js so it never re-attaches. Verified: applyVersionEnvelope is absent from both dist/lib/index.d.ts and dist/lib/protocols/index.d.ts after this change; runtime export remains for the regression tests. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
bokelley
added a commit
to adcontextprotocol/adcp-client
that referenced
this pull request
Apr 30, 2026
* feat(server): single-field VERSION_UNSUPPORTED check (#1075) Closes the spec-conformance gap surfaced during PR #1073 review. `createAdcpServer`'s field-disagreement check (PR #1067) only fired when both `adcp_version` and `adcp_major_version` were present and the majors disagreed. A buyer sending only `adcp_major_version: 99` (or only `adcp_version: "99.0"`) bypassed the cross-check and the value rode into the handler unvalidated. Server-side: - New file-private helpers `getAdvertisedSupportedMajors` and `buildSupportedVersionsList`. Union the parsed majors from `capConfig.major_versions` (deprecated integer list) and `capConfig.supported_versions` (release-precision strings, AdCP 3.1+ per spec PR adcontextprotocol/adcp#3493), with fallback to the server pin's major when both lists are absent. - New single-field rejection runs after the existing dual-field check. Resolves the effective major from whichever envelope field the buyer set, returns VERSION_UNSUPPORTED with `details.supported_versions` populated when the major is outside the seller's advertised window. - Dual-field check also gains the `details.supported_versions` echo so buyers can downgrade and retry after either kind of failure. - New `AdcpCapabilitiesConfig.supported_versions?: string[]` for 3.1+ sellers to declare release-precision strings. Conformance runner (test isolation fix): - `runToolFuzz` overwrites `adcp_major_version` / `adcp_version` on each sample before dispatch. The buyer SDK already auto-fills these via `applyVersionEnvelope` (PR #1073); without the runner pin, the schema-driven 1-99 integer arb would generate out-of-window claims on most samples and trigger the new server check before any handler bug could surface. Pinning at the runner (not the arbitrary) keeps `schemaToArbitrary` pure and existing schema-validity threshold tests undisturbed. Tests: - New `version-unsupported-server.test.js` (9 tests) covers single-field integer + string rejections, the 3.1-style `supported_versions: ['3.1.0']` seller path, multi-version sellers, fallback-to-server-pin, and regression coverage for the dual-field disagreement check. Combined with #1073, fully unblocks the storyboard skip in adcontextprotocol/adcp#3626 — the framework's own seller fixture now passes the bundled `error_compliance/unsupported_major_version` step. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(server): address bokelley review comments on #1080 - Add JSDoc note to `buildSupportedVersionsList` explaining why strings are preferred over integers (asymmetric precedence vs `getAdvertisedSupportedMajors`). - Replace hardcoded `adcp_major_version: 3, adcp_version: '3.0'` pin in `runToolFuzz` with `ADCP_MAJOR_VERSION` (single-field; no string drift). - Document the additive `details.supported_versions` behavior change on the dual-field path in the changeset. https://claude.ai/code/session_0184yyiQfCKErdpCxm4PjxnX --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds release-precision version negotiation to AdCP via a new
adcp_versiontop-level envelope field on every request and response. Buyers send their release pin (e.g."3.0","3.1"); sellers echo the release they actually served. Augments the existing major-precisionadcp_major_version(integer) with finer precision and adds response-side echo, which the spec lacked.Full proposal:
specs/version-negotiation.md.Why now
Major-precision is no longer the actual interop boundary in practice — clients and servers are matched at release boundaries. 3.0.1 is GA, 3.1 is heading to beta, and the additive-only forward-compat-within-major promise doesn't hold cleanly when the spec ships breaking fixes inside a major. Release-precision negotiation is what client SDKs need to do Stripe-style per-instance pinning, and what testing harnesses need for cross-release interop in one process.
What's in this PR
This is both the RFC and the implementation — fully additive on the wire, ready to ship as part of a 3.1 release.
core/version-envelope.jsoncarryingadcp_version(release-precision string) andadcp_major_version(deprecated integer). Composed viaallOf $reffrom every AdCP request and response — single source of truth, no per-schema duplication.adcp.supported_versionsandadcp.build_versionon the capabilities response. Marks legacyadcp.major_versionsas deprecated.build_versionpattern is full semver with optional pre-release and build-metadata segments per semver §9–§10.error-details/version-unsupported.json— standardizederror.datashape for the existingVERSION_UNSUPPORTEDcode (no new code, just structured details). Linked from theVERSION_UNSUPPORTEDenum description.docs/reference/versioning.mdxupdated with the bidirectional negotiation algorithm, the patch-not-negotiated rule, the deprecation timeline, the MCPprotocolVersionlayering note, and worked examples for pre-release pin matching.tests/lint-version-envelope.test.cjsenforces the composition invariant: schemas using the envelopeallOf $refMUST have permissiveadditionalPropertiesat root (draft-07 limitation; strict variant returns at draft 2019-09 — see Migrate JSON Schema source from draft-07 to 2019-09 (4.0) #3534).adcontextprotocol.Key decisions
extensions.*and not in transport headers. Version is meta-protocol, not a feature. Headers don't survive stdio-MCP. A versioned extension surface can't carry the signal that selects its own version.adcp_version) on both sides — semantics fall out of context (request body vs response body). MirrorsStripe-Version."3.0","3.1"); patches surface as advisorybuild_versionon capabilities. Per the spec's three-tier model, patches by definition introduce no contract change — making them part of negotiation bakes a category mistake into the spec.VERSION_UNSUPPORTEDwith a richererror.data(no new error code).adcp_major_versionandadcp.major_versionsthrough 3.x, remove in 4.0.adcp_versionis the release served, never the seller's own latest release. A 3.1 seller serving a 3.0 buyer at 3.0 echoes"3.0".adcp_versionandadcp_major_versionand the majors disagree, the server MUST returnVERSION_UNSUPPORTED.Backwards compatibility
Fully additive:
adcp_versionignore it (additionalProperties: true) and fall back to the existingadcp_major_versionor default. Existing clients that don't read responseadcp_versionkeep working.adcp.major_versions(integer) and the newsupported_versions(string) through 3.x.VERSION_UNSUPPORTEDcallers see a richererror.datapayload but the code is unchanged.One scoped behavior change: 17 request schemas (under
collection/,governance/,property/,tmp/) previously declaredadditionalProperties: falseat root. TheallOf $refcomposition pattern requires permissive parents in draft-07 (allOf does not bypass the parent's strict mode), so this PR flips them totrue. Strict request validation across these schemas returns at draft 2019-09 viaunevaluatedProperties: false— see #3534.Migration
Spec stays at SHOULD on both sides through all of 3.x — consistent with the 3.x stability guarantee that fields don't graduate optional → required within a major. The AdCP compliance grader carries adoption pressure within 3.x.
adcp_version(withadcp_major_versionmirror).adcp_version. SHOULD emitsupported_versionson capabilities.adcp_versionor don't emitsupported_versionson capabilities.adcp_version.adcp_major_versionremoved.adcp_version.adcp.major_versionsandextensions.adcp.adcp_versionremoved.Out of scope (future RFCs)
min/max). Single-version pin only.VERSION_UNSUPPORTED.unevaluatedProperties: false— tracked at Migrate JSON Schema source from draft-07 to 2019-09 (4.0) #3534.allOf $refenvelope factoring foridempotency_key,governance_context,context,ext— tracked at Catalog envelope-level fields and migrate to allOf shared-envelope pattern #3535.Test plan
npm run build:schemasclean.npm run build:complianceclean.test:schemas(7),test:examples(34),test:json-schema(255 parsed JSON blocks),test:composed(32),test:extensions(20),test:version-envelope(new lint enforcing theallOfcomposition invariant).collection/,governance/,property/,tmp/) now acceptadcp_versionandadcp_major_versionafter theadditionalProperties: trueflip.Behavioral conformance — the resolution algorithm (cross-major rejection, in-major downshift, pre-release exact-match, dual-emit field-disagreement, response echo) is server-side behavior, not validatable from the JSON Schema alone. End-to-end coverage lives in the downstream SDK conformance suites:
@adcp/clientalready merged.adcpPR Fix semantic version sorting for agreements #294 (54 tests).The spec PR ships definitions; the contract is exercised end-to-end via SDK testing.
Reviewer pointers
Triage and reviewer feedback already addressed in commits on this branch:
allOf $reftocore/version-envelope.json.VERSION_UNSUPPORTED; pre-release exact-match row added; field-disagreement →VERSION_UNSUPPORTED).build_versionpinned to semver §9–§10 format with build-metadata segment.protocolVersionvs AdCPadcp_versionlayering note in both RFC and doc.Open question for protocol-WG eyes: anything in the SHOULD-with-grader-gating model that doesn't match how AAO actually runs certification.