Skip to content

feat(adcp): per-instance adcpVersion string API with cross-major validation (#16)#19

Merged
MichielDean merged 1 commit into
track3/transportfrom
feature/issue-16-adcp-version-string-api
May 19, 2026
Merged

feat(adcp): per-instance adcpVersion string API with cross-major validation (#16)#19
MichielDean merged 1 commit into
track3/transportfrom
feature/issue-16-adcp-version-string-api

Conversation

@MichielDean
Copy link
Copy Markdown
Collaborator

Closes #16

What this does

Implements the Stripe-model per-instance adcpVersion option so a single process can talk to agents running different protocol versions.

API surface

// String convenience (new) — release precision like TS/Python SDKs
AdcpClient.builder()
    .agent(config)
    .adcpVersion("3.0")   // parsed to AdcpVersion(3, "3.0")
    .build();

// AgentConfig also gets the string overload
AgentConfig.builder()
    .id("seller")
    .agentUri(uri)
    .adcpVersion("3.1")
    .build();

// Cross-major pin rejected at build() time, before any network request
AdcpClient.builder()
    .adcpVersion("2.0")   // throws ConfigurationError — SDK is major 3
    .build();

AdcpVersion.of(String)

New static factory parses "3.0" / "3.1" (release precision) into the existing AdcpVersion record. Follows the same convention as the TS and Python SDKs' adcpVersion constructor option.

Build-time AdcpSdkVersion.java

Generated from ADCP_VERSION at build time — no manually-maintained compatibility list. ADCP_VERSION = "3.0.11"SDK_MAJOR_VERSION = 3, SDK_RELEASE_VERSION = "3.0". When the protocol bumps to v4, updating ADCP_VERSION automatically updates the validation constant; no code change needed.

Cross-major validation

AdcpClient.Builder.build() checks adcpVersion.majorVersion() == AdcpSdkVersion.SDK_MAJOR_VERSION. Mismatch throws ConfigurationError with a clear message before any network activity.

Acceptance criteria from #16

  • AdcpClient.builder().adcpVersion("3.0").build() accepted
  • Outbound requests carry adcp_version at release precision ("3.0", "3.1")
  • Cross-major mismatch raises ConfigurationError before send
  • Compatibility derived from ADCP_VERSION at build time (not hardcoded)

Notes

  • Targets track3/transport because AdcpClient doesn't exist on main yet
  • AdcpSdkVersion.java is generated into build/generated/ and not checked in

…validation

Implements issue #16 — Stripe-model per-instance adcpVersion option.

Changes:
- AdcpVersion.of(String): parse release-precision string "3.0" / "3.1"
  into an AdcpVersion. Companion to the existing AdcpVersion(int, String)
  constructor; follows the TS/Python SDK convention for caller-site pinning.

- Build-time AdcpSdkVersion.java: generated from ADCP_VERSION at build time
  (major=3, release="3.0" from "3.0.11"). Eliminates the manually-maintained
  COMPATIBLE_ADCP_VERSIONS list that the Python SDK got burned by — updating
  ADCP_VERSION automatically updates the compatibility constant.

- AdcpClient.Builder.adcpVersion(String): convenience overload that calls
  AdcpVersion.of(). Cross-major pins (e.g. "2.0" on a major-3 SDK) throw
  ConfigurationError at build() time, before any network request.

- AgentConfig.Builder.adcpVersion(String): same string-based overload.

Tests: AdcpVersion.of() parsing, AdcpSdkVersion constant invariants,
AdcpClient cross-major rejection, string-builder acceptance.

Acceptance criteria from #16:
  ✓ AdcpClient.builder().adcpVersion("3.0").build() accepted
  ✓ Outbound requests carry adcp_version at release precision
  ✓ Cross-major mismatch raises ConfigurationError before send
  ✓ Compatibility derived from ADCP_VERSION at build time

jira-issue: ADCP-0017

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@MichielDean MichielDean marked this pull request as ready for review May 19, 2026 17:21
@MichielDean MichielDean requested a review from bokelley as a code owner May 19, 2026 17:21
@MichielDean MichielDean changed the base branch from track3/transport to main May 19, 2026 17:22
@MichielDean MichielDean changed the base branch from main to track3/transport May 19, 2026 17:22
@MichielDean MichielDean changed the base branch from track3/transport to main May 19, 2026 17:29
@MichielDean MichielDean changed the base branch from main to track3/transport May 19, 2026 17:30
@MichielDean MichielDean merged commit b80aab4 into track3/transport May 19, 2026
10 checks passed
@MichielDean MichielDean deleted the feature/issue-16-adcp-version-string-api branch May 19, 2026 17:31
MichielDean added a commit that referenced this pull request May 19, 2026
…3) (#18)

* feat(transport): Phase 1+2 — AdcpHttpClient and error taxonomy

Phase 1: SSRF-safe HTTP client
- AdcpHttpClient with DNS-pin, body cap, redirect:NEVER
- AdcpHttpResponse record with truncation tracking
- DnsPinResolver for DNS resolution + SSRF validation
- SsrfBlockedException for blocked requests

Phase 2: Error taxonomy (14 sealed subclasses of AdcpError)
- ProtocolError, AuthenticationRequiredError, TaskTimeoutError,
  TaskAbortedError, DeferredTaskError, ValidationError,
  ConfigurationError, VersionUnsupportedError, AgentNotFoundError,
  UnsupportedTaskError, FeatureUnsupportedError, ResponseTooLargeError,
  IdempotencyConflictError, IdempotencyExpiredError
- AuthChallengeInfo + OAuthMetadataInfo records
- WwwAuthenticateParser (RFC 9110 §11.6.1)
- Package-info.java for error and auth packages

Also updates ROADMAP.md Track 3 owner and tracks claimed.

jira-issue: ADCP-0017

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat(transport): Phase 3 — AgentConfig, auth types, and token resolver

- AgentConfig record with builder, auth exclusivity validation,
  and static MCP factory methods
- Protocol enum (MCP, A2A)
- AdcpVersion record with V3/V3_1 constants
- BasicCredentials, OAuthClientCredentials, OAuthTokens records
- AuthTokenResolver: Bearer, Basic, OAuth token → header resolution
  with x-adcp-auth backward compatibility header for static tokens

jira-issue: ADCP-0017

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat(transport): Phase 4 — MCP caller, connection manager, version envelope

- McpConnectionManager: LRU-cached MCP connections (max 20),
  StreamableHTTP → SSE fallback, evict-and-retry on transport errors
- McpCaller: callTool dispatch, content extraction, deserialization
- ProtocolClient: central dispatch point for all tool calls,
  SSRF URL validation, auth header injection, version envelope merge
- VersionEnvelope: adcp_major_version + adcp_version injection,
  caller args win on collision (conformance override)
- CallToolOptions: per-call timeout, body cap, validation toggle
- DnsPinResolver: made public for cross-package SSRF validation
- adcp/build.gradle.kts: added mcp-core + mcp-json-jackson2 (impl),
  excluded transitive json-schema-validator 2.x to keep pinned 1.5.x

jira-issue: ADCP-0017

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat(transport): Phase 5 — AdcpClient with generic callTool dispatch

- AdcpClient: single-agent client, builder pattern, AutoCloseable
- Generic callTool(toolName, args, responseType) method
- callNamedTool(toolName, typedRequest, responseType) for type safety
- Builder: agent, adcpVersion, objectMapper, ssrfPolicy
- Lifecycle: close() evicts cached MCP connections

jira-issue: ADCP-0017

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat(server): add MCP server-side transport (AdcpPlatform SPI + builder)

Implements Phase 6 of Track 3: the agent-side MCP server wiring.

- AdcpContext: per-request context record (version, headers, principal)
- AdcpPlatform: abstract SPI that agent adopters extend; supportedTools()
  + handleTool() dispatch. Only supported tools are advertised via
  MCP tools/list.
- AdcpServerBuilder: wires AdcpPlatform to McpSyncServer via
  McpServer.sync(transport).toolCall(...). Extracts version envelope
  from inbound args, serialises responses as TextContent.
- Tests for platform dispatch + supportedTools + error paths.

jira-issue: ADCP-0017

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* test(testing): add integration tests for server builder and client

Phase 8 of Track 3: integration testing.

- ServerBuilderRoundTripTest: 9 tests validating AdcpPlatform →
  AdcpServerBuilder wiring, tool dispatch, error handling, context
  propagation, and version extraction.
- AdcpClientIntegrationTest: client integration tests against the
  @adcp/sdk/mock-server sidecar (enabled when ADCP_MOCK_SERVER_URL
  is set in CI).
- Add adcp-server as testImplementation dep of adcp-testing module.

jira-issue: ADCP-0017

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(transport): audit fixes for security, thread safety, and correctness

Address 22 findings from comprehensive code audit:

CRITICAL:
- McpConnectionManager: fix check-then-act race with ReentrantLock,
  pass auth headers via httpRequestCustomizer, pass connectTimeout
  to transport builders, add volatile closed flag, clean up
  knownStreamableUrls on evict/close
- AdcpHttpClient.close(): call httpClient.close() (was no-op)
- Credential records: override toString() to redact secrets in
  BasicCredentials, OAuthClientCredentials, OAuthTokens
- SsrfBlockedException: remove host from getMessage() to prevent
  information leakage
- AdcpHttpClient.pinUri(): stop rewriting URI with IP (broke HTTPS
  SNI/TLS); validate addresses but keep original hostname

HIGH:
- ProtocolClient.computeTokenHash(): use SHA-256 instead of
  String.hashCode() (32-bit collision risk)
- AdcpServerBuilder: use ObjectMapper for error JSON serialization
  to prevent JSON injection; strip version envelope fields from
  args before passing to platform
- AdcpClient.toArgs(): reuse ObjectMapper field instead of creating
  new instance per call
- McpCaller.extractResponse(): check result.isError() before
  deserializing as success
- AdcpHttpClient: filter protected headers (Host, User-Agent,
  Content-Length, Transfer-Encoding) from caller-supplied map
- BasicCredentials: reject colon in username per RFC 7617 §2
- AuthChallengeInfo: add null validation on scheme
- OAuthMetadataInfo: add null validation on required fields
- AdcpHttpClient.pinUri(): use syntactic IP-literal check instead
  of DNS call for literal addresses

MEDIUM:
- ProtocolClient retry: only retry transport errors (IOException,
  timeout), chain original exception as suppressed
- McpConnectionManager.isAuthError(): match specific patterns
  (HTTP 401, status: 401) instead of bare substring "401"

jira-issue: ADCP-0017

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(security): comprehensive security hardening across transport layer

Address findings from deep security audit of Track 3 code:

HIGH — Must Fix:
- AgentConfig: override toString() to redact authToken and
  webhookSecret (CWE-532 info disclosure via auto-generated toString)
- AgentConfig: reject authToken with CR/LF characters to prevent
  header injection (CWE-113)
- AdcpObjectMapperFactory: reduce limits from 100MB/2000 depth to
  10MB/200 depth to prevent DoS via oversized payloads (CWE-400)
- McpConnectionManager: add CRLF sanitization and protected-header
  filtering on MCP transport headers (CWE-113)
- ProtocolClient.validateUrl(): perform DNS resolution and SSRF
  policy validation for MCP transport path — was previously skipped
  entirely, allowing SSRF via private address hostnames (CWE-918)

MEDIUM — Should Fix:
- ProtocolClient.computeTokenHash(): use full SHA-256 output instead
  of truncated 8 bytes to prevent cache key collisions (CWE-328)
- ProtocolClient: enforce auth-last header merge ordering so
  extraHeaders cannot override Authorization (CWE-287)
- McpCaller: sanitize and truncate remote error text to 500 chars,
  strip control characters to prevent injection into LLM context
- AdcpServerBuilder: split error handling into known (AdcpError →
  safe to surface) vs unknown (Exception → "internal error" only)
  to prevent internal details leaking to remote callers (CWE-209)
- AdcpServerBuilder.extractVersion(): validate major version >= 3
  to prevent protocol downgrade attacks (CWE-757)
- AdcpHttpResponse: defensive clone of body byte[] in compact
  constructor to prevent mutation (CWE-374)
- McpConnectionManager: move closed check inside lock to prevent
  connection creation race after close() (CWE-362)
- AdcpHttpClient/McpConnectionManager: expand protected headers
  to include connection/upgrade
- SsrfBlockedException.host(): restrict to package-private visibility
  to limit hostname exposure (CWE-209)

LOW — Cleanup:
- McpConnectionManager: replace ConcurrentHashMap-backed
  knownStreamableUrls with plain HashSet (always accessed under lock)

jira-issue: ADCP-0017

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(transport): comprehensive audit fixes for correctness and API design

Address findings from deep code review of Track 3 implementation:

CRITICAL:
- McpConnectionManager.close(): move `closed = true` inside lock to
  prevent race where concurrent getOrConnect() sees inconsistent state
  (CWE-362)

HIGH:
- AdcpHttpResponse.body(): override record accessor to return defensive
  copy — the compact constructor cloned on construction but the accessor
  still leaked a reference to the internal array (CWE-374)
- AdcpServerBuilder: use platform.toolDescriptions() for MCP tool
  descriptions instead of using tool name as description (hurts LLM
  tool selection)
- AdcpServerBuilder: use safe constant in JSON fallback error path
  instead of string-concatenating e.code() (CWE-116 JSON injection)
- AdcpClient: fail fast with FeatureUnsupportedError when constructed
  with Protocol.A2A instead of silently creating MCP infrastructure
  and failing at callTool() time

MEDIUM:
- AuthChallengeInfo: enforce lowercase scheme in compact constructor
  to match documented contract
- AdcpVersion: validate minorVersion starts with majorVersion to
  prevent inconsistent version objects (e.g., major=3, minor="4.1")
- AgentConfig.toString(): redact extraHeaders values (may contain
  API keys like X-Api-Key)
- Extract shared ProtectedHeaders utility to eliminate duplicate
  PROTECTED_HEADERS constant between AdcpHttpClient and
  McpConnectionManager (drift risk)
- McpConnectionManager.connectWithFallback: extract buildAndInit()
  helper to eliminate triple-duplicated transport construction code
- McpCaller.extractResponse: track first parse error and attach as
  suppressed exception for better diagnostics
- CallToolOptions.Builder.maxResponseBytes: add validation rejecting
  zero and negative values
- McpConnectionManager.isAuthError: check McpError type first before
  falling back to string matching; add TODO for typed status exposure

LOW:
- McpConnectionManager.evictOldest: also clear knownStreamableUrls
  entry during LRU eviction to prevent stale transport preference

Tests: 5 new tests (178 total, 168 passing — 10 pre-existing schema
failures unrelated to Track 3).

jira-issue: ADCP-0017

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(transport): final audit fixes — locale, CRLF, resource leak, cause chain

Address 4 findings from final comprehensive audit:

- WwwAuthenticateParser: use Locale.ROOT in toLowerCase() to prevent
  Turkish-locale JVMs producing incorrect scheme strings like "basıc"
  instead of "basic" (CWE-178 improper case handling)
- OAuthTokens: reject CR/LF in accessToken to match authToken
  validation parity — prevents silent header injection that would
  result in an unauthenticated request instead of a clear error
- McpConnectionManager.buildAndInit: close McpSyncClient if
  initialize() throws to prevent resource leak of HttpClient thread
  pools on repeated connection failures
- AuthenticationRequiredError: add cause-carrying constructor and
  pass original exception from McpConnectionManager auth detection
  so stack traces are preserved for debugging

jira-issue: ADCP-0017

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(security): second security audit — SSRF bypass, DoS cap, info leak

- HIGH: Fix IPv4-compatible IPv6 SSRF bypass (::a.b.c.d form)
  StrictSsrfPolicy.unmapIpv4Mapped() now unwraps both ::ffff:a.b.c.d
  (mapped) and ::a.b.c.d (compatible) forms before range checking
- MEDIUM: Add 10MB content size cap in McpCaller.extractResponse()
  to prevent OOM from malicious agents returning oversized TextContent
- MEDIUM: Stop leaking AdcpError.getMessage() to remote callers —
  AdcpServerBuilder now returns only e.code() in error responses
- LOW: Validate extraHeaders for CRLF at AgentConfig construction
  (fail-fast instead of relying solely on downstream sanitization)
- LOW: Validate AdcpVersion.minorVersion format with regex + length
  cap to prevent log injection via crafted version strings
- LOW: Warn via SLF4J when credentials are configured for plaintext
  HTTP agent URIs
- Docs: CallToolOptions Javadoc documents which fields are reserved
  (timeout, maxResponseBytes not yet wired in v0.1)

jira-issue: ADCP-0017

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(transport): third code audit — MCP spec compliance, correctness, API design

- HIGH: Add inputSchema to MCP tool registration (MCP spec requires it)
- HIGH: Fix WwwAuthenticateParser Locale.ROOT on param key lowercasing
- HIGH: Fix knownStreamableUrls state leak across different token hashes
  (now keyed on full cache key instead of URL alone)
- HIGH: Remove unused imports and fix misleading Javadoc in DnsPinResolver
- HIGH: Fix AdcpPlatform Javadoc (falsely claimed reflection-based discovery)
- MEDIUM: Add AdcpHttpResponse.equals/hashCode using Arrays.equals for body
- MEDIUM: Redact all credentials in AgentConfig.toString() (basicAuth,
  oauthClientCredentials, oauthTokens were previously shown in clear)
- MEDIUM: Add authorization to ProtectedHeaders (prevent silent override)
- MEDIUM: Fix error response shape (was {error:code, code:code}, now
  {error:code, message:msg}) for AdcpError cases
- MEDIUM: Parse string major version values in extractVersion
- MEDIUM: Include oauthClientCredentials in computeTokenHash
- MEDIUM: Use ConfigurationError (not ProtocolError) for builder validation
- MEDIUM: Log warning when non-default CallToolOptions are passed (v0.1)

jira-issue: ADCP-0017

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(security): fourth adversarial audit — cache isolation, stream leak, DoS cap

- Include extraHeaders hash in MCP connection cache key to prevent
  cross-tenant header leakage in multi-tenant scenarios
- Revert authorization from ProtectedHeaders (broke SDK auth path)
- Fix InputStream leak in AdcpHttpClient when readBodyWithCap throws
- Skip null TextContent.text() entries in McpCaller instead of NPE
- Cap adcp_version string to 20 chars to prevent unbounded allocation
- Reject major version > 99 as unsupported

jira-issue: ADCP-0017

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(transport): fifth adversarial audit — lock contention, OOM guard, retry logic

- Replace global lock with per-key striped locking in McpConnectionManager
  so one slow/unreachable agent doesn't block all others (HEAD-OF-LINE fix)
- Make knownStreamableKeys a ConcurrentHashMap.KeySetView for thread safety
- Cap maxResponseBytes at 64 MB to prevent OOM from misconfigured caps
- Replace fragile contains("Transport") with cause-chain IOException walk
  for correct retry decisions

jira-issue: ADCP-0017

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(transport): prevent unbounded keyLocks growth on token rotation

Clean up per-key locks after connection attempt completes and clear
the map on close(). Without this, each OAuth token rotation created
an orphaned lock entry that persisted for process lifetime.

jira-issue: ADCP-0017

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(transport): sixth adversarial audit — lock race, close race, error leak

- Guard keyLocks.remove() with hasQueuedThreads() to prevent duplicate
  connections when evict+reconnect races with a new per-key lock
- Check closed flag after connecting before inserting into cache to
  prevent resource leak when close() races with connect
- Sanitize server-side AdcpError messages (truncate 500 chars, strip
  control chars) before sending to remote callers (CWE-209)

jira-issue: ADCP-0017

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(transport): seventh adversarial audit — striped semaphores, request timeout, error sanitization

Major concurrency redesign of McpConnectionManager:
- Replace per-key ReentrantLock map with fixed-size striped Semaphore[32]
  pool, eliminating the keyLocks cleanup race and unbounded growth
- Semaphore.acquire() is virtual-thread-friendly (no carrier pinning),
  fixing performance DoS under JDK 21 virtual threads
- Add 30s request timeout via requestBuilder on both StreamableHTTP and
  SSE transports, preventing half-open connection DoS where a malicious
  agent accepts TCP but never responds to MCP initialize

Server-side error handling:
- Sanitize AdcpError messages before sending to remote callers: truncate
  to 500 chars and strip control characters to prevent info leakage
  (CWE-209)

jira-issue: ADCP-0017

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: address bokelley PR review findings and fix CI lockfiles

Blockers:
- B1: DNS pinning via URI rewriting + Host header injection
- B2: ProtectedHeaders for auth/cookie, filtered before auth merge
- B3: McpCaller reads structuredContent first, falls back to content[]
- B4: HEAD probe for WWW-Authenticate on auth errors
- B5: OAuth CC throws FeatureUnsupportedError (not yet implemented)
- B6: ServerBuilderRoundTripTest with StubMcpTransport
- B7: AdcpClientIntegrationTest with stronger spec assertions

Majors:
- M1: WwwAuthenticateParser quoted-pair fix + 16-param cap
- M2: IPv6 SSRF: 6to4, Teredo, NAT64, octal IP rejection
- M3: AdcpPlatform.handleTool Object → Map<String,Object>
- M5: invalidateForAgent for cache eviction on token rotation
- M6: isAuthError word-bounded pattern matching
- M7: toolSchemas() SPI on AdcpPlatform for typed input schemas
- M8: ValidationError field → path list + schemaUri
- M9: VersionEnvelope SDK wins over caller
- M10: computeTokenHash HMAC with per-process random key

Minors:
- N1: null callerArgs handled in VersionEnvelope
- N2: BasicCredentials allows blank password
- N3: A2A rejection deduplication (kept in ProtocolClient only)

CI:
- Updated dependency lockfiles for 4 modules (MCP SDK transitives)
- connectWithFallback TODO for content-type probe (M4 deferred)

jira-issue: ADCP-0017

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: guard MCP integration test behind ADCP_MCP_SERVER_URL

The current mock-server sidecar is a REST stub, not an MCP server,
so callTool tests fail when run against it. Guard the MCP-specific
test behind a separate env var until the mock-server gains MCP
support.

jira-issue: ADCP-0017

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* docs: add cosign as a build prerequisite

The schema bundle fetch task shells out to `cosign verify-blob` to
verify Sigstore signatures. Without cosign installed, all 10 schema
tests fail with "Cannot run program cosign". Document the requirement
in CONTRIBUTING.md and CLAUDE.md.

jira-issue: ADCP-0017

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: address remaining bokelley review findings (M4, N4, N5, N6)

M4: Replace exception-based StreamableHTTP→SSE fallback with a
content-type probe (POST with initialize to detect transport).
Known-good endpoints skip the probe. Falls back to SSE when the
probe gets 405 or non-JSON response.

N4: Add requireHttps(boolean) to AdcpHttpClient.Builder. When true,
rejects plain http:// for non-loopback hosts with a clear error.
Defaults to false for backward compat. Localhost is always exempt.

N5: Harden ObjectMapper in McpCaller — defensively disables default
typing on a copy to prevent polymorphic deserialization attacks when
responseType is Object.class or Map.class.

N6: Change extractVersion to default to v1 semantics for major<3
instead of throwing VersionUnsupportedError, matching Python
adcp.server back-compat behavior.

Also cleans up duplicate TODO comments in McpConnectionManager (B4).

jira-issue: ADCP-0017

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* test(server): add handleToolCall unit tests for B6 completeness

Extract handleToolCall and extractVersion to package-private visibility
so they can be tested directly from AdcpServerBuilderTest. The new test
class exercises:

- Tool dispatch through the builder's handler
- Version envelope stripping before platform dispatch
- Version extraction into AdcpContext
- AdcpError wrapping with stable error codes
- Unexpected exception wrapping without detail leakage
- Null arguments handling
- extractVersion edge cases (string/int major, back-compat, oversized minor)

This completes B6 from bokelley's review: the builder's handleToolCall
code path (version-envelope strip, error wrapping, handler dispatch) is
now fully exercised by tests, not just assertNotNull(server).

jira-issue: ADCP-0017

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: add null guard on AdcpClient.callTool args parameter

callTool now accepts @nullable args — null is normalised to Map.of()
before reaching ProtocolClient/VersionEnvelope. Adds a test verifying
null args does not NPE.

jira-issue: ADCP-0017

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat(adcp): add per-instance adcpVersion string API with cross-major validation (#19)

Implements issue #16 — Stripe-model per-instance adcpVersion option.

Changes:
- AdcpVersion.of(String): parse release-precision string "3.0" / "3.1"
  into an AdcpVersion. Companion to the existing AdcpVersion(int, String)
  constructor; follows the TS/Python SDK convention for caller-site pinning.

- Build-time AdcpSdkVersion.java: generated from ADCP_VERSION at build time
  (major=3, release="3.0" from "3.0.11"). Eliminates the manually-maintained
  COMPATIBLE_ADCP_VERSIONS list that the Python SDK got burned by — updating
  ADCP_VERSION automatically updates the compatibility constant.

- AdcpClient.Builder.adcpVersion(String): convenience overload that calls
  AdcpVersion.of(). Cross-major pins (e.g. "2.0" on a major-3 SDK) throw
  ConfigurationError at build() time, before any network request.

- AgentConfig.Builder.adcpVersion(String): same string-based overload.

Tests: AdcpVersion.of() parsing, AdcpSdkVersion constant invariants,
AdcpClient cross-major rejection, string-builder acceptance.

Acceptance criteria from #16:
  ✓ AdcpClient.builder().adcpVersion("3.0").build() accepted
  ✓ Outbound requests carry adcp_version at release precision
  ✓ Cross-major mismatch raises ConfigurationError before send
  ✓ Compatibility derived from ADCP_VERSION at build time

jira-issue: ADCP-0017

Co-authored-by: Bugher-Michiel-1124273_TDX <Michiel.Bugher@Tritondigital.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(transport): address B1 DNS pinning regression from bokelley review

Remove URI rewriting that broke TLS hostname verification:
- DnsPinResolver no longer rewrites URIs to IP literals; validates at
  resolve time and returns the original URI so HTTPS SNI works correctly
- AdcpHttpClient.pinUri() renamed to validateUri(), scheme check added
- Host header injection removed (no longer needed without rewriting)

Route all MCP probes through AdcpHttpClient for SSRF defense:
- McpConnectionManager accepts AdcpHttpClient via new constructor
- probeSupportsStreamableHttp uses adcpHttpClient.post() with ping
  payload instead of raw HttpClient with initialize (avoids half-handshake)
- probeAndBuildAuthError routes through adcpHttpClient.send() with
  OPTIONS fallback when HEAD returns 405
- MCP transports use clientBuilder(adcpHttpClient.newMcpClientBuilder())
  instead of customizeClient

Additional fixes from review:
- ProtocolClient.validateUrl rejects non-http/https schemes
- AdcpClient.Builder exposes requestTimeout(Duration) for long-poll calls
- AuthenticationRequiredError.challenge javadoc documents null semantics
- AdcpClient.close() properly cascades to AdcpHttpClient

jira-issue: ADCP-0017

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Bugher-Michiel-1124273_TDX <Michiel.Bugher@Tritondigital.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
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.

caller: per-instance adcpVersion builder option

2 participants