Skip to content

feat(server): /.well-known/adcp-agents.json discovery endpoint (#381)#406

Merged
bokelley merged 3 commits intomainfrom
bokelley/issue-381-discovery-doc
May 3, 2026
Merged

feat(server): /.well-known/adcp-agents.json discovery endpoint (#381)#406
bokelley merged 3 commits intomainfrom
bokelley/issue-381-discovery-doc

Conversation

@bokelley
Copy link
Copy Markdown
Contributor

@bokelley bokelley commented May 3, 2026

Summary

  • Implements AdCP PR #3903 (commit 5c3e3e626) — every AdCP host now publishes an origin-scoped multi-agent topology manifest at /.well-known/adcp-agents.json.
  • Wires the route into serve() for every HTTP transport (streamable-http, a2a, both); stdio has no HTTP surface and skips the route.
  • Manifest builder lives in adcp.server.discovery as a pure function (also exposed via make_discovery_route) so adopters can publish a static manifest from CI.

What buyers see

$ curl http://localhost:3001/.well-known/adcp-agents.json
{
  "$schema": "/schemas/adcp-agents.json",
  "version": "1.0",
  "agents": [
    {
      "agent_id": "my-seller-mcp",
      "url": "https://sales.example.com/mcp",
      "transport": "mcp",
      "specialisms": ["sales-non-guaranteed"]
    },
    {
      "agent_id": "my-seller-a2a",
      "url": "https://sales.example.com",
      "transport": "a2a",
      "specialisms": ["sales-non-guaranteed"]
    }
  ],
  "contact": {"name": "my-seller"},
  "last_updated": "2026-05-02T00:00:00Z"
}

New serve() kwargs

  • base_url — public origin used in manifest URLs. Defaults to http://<bind-host>:<port>; production deployments behind a TLS-terminating proxy SHOULD set this.
  • specialisms — list of AdCP specialism tags (e.g. ["sales-non-guaranteed"]). Falls back to a placeholder; adopters who know their specialism SHOULD pass it.
  • description — human-readable description surfaced in operator UIs.

Known gaps / TODOs

  • Specialism inferencebuild_manifest() currently accepts specialisms verbatim. The handler's advertised tools could in principle infer a default specialism (e.g. get_productssales-non-guaranteed), but that's a non-trivial mapping and would conflate "implements the tool" with "operator declares the specialism." Left as a TODO(#381) in discovery.py; placeholder works for adopters who haven't classified yet.
  • auth_hint — schema-optional; not surfaced because the SDK doesn't own auth wiring. Adopters who want to advertise it can build their own manifest via build_manifest() and serve it through a custom route.

Test plan

  • ruff check src/
  • mypy src/adcp/
  • pytest tests/ -k "discovery" (31 passed, 9 skipped)
  • pytest tests/test_unified_mcp_a2a.py tests/test_serve_dx_polish.py tests/test_serve_asgi_middleware.py tests/test_a2a_server.py tests/test_server_dx.py tests/test_server_framework.py tests/test_server_helpers.py (194 passed) — no regressions
  • New tests/test_discovery_endpoint.py covers GET on each transport, JSON-schema validation, POST falls through, agent_id normalization, builder unit tests
  • CI green

🤖 Generated with Claude Code

bokelley added a commit that referenced this pull request May 3, 2026
…(PR #406 fix-pack)

Discovery manifest must publish https:// URLs per the AdCP schema's
``^https://`` pattern (loopback exception only). resolve_base_url now
raises at boot when a non-loopback bind has no base_url, or when an
explicit http:// base_url is paired with a non-loopback host — silent
mis-publication survives in CDNs and conformance reports for hours.

Also: strip separators before the 64-char agent_id cap so truncation
never lands on a stripped char, and quantize last_updated to whole-hour
granularity so consecutive requests within an hour are byte-identical
(HTTP caches collapse the duplicates).

Tests restore the ``^https://`` pattern in the inlined schema and add
coverage for the new boot-time errors.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@bokelley bokelley closed this May 3, 2026
@bokelley bokelley reopened this May 3, 2026
bokelley and others added 3 commits May 2, 2026 22:26
Per AdCP PR #3903, every AdCP host publishes an origin-scoped manifest
enumerating the agents it serves at /.well-known/adcp-agents.json.
serve() now exposes this on every HTTP transport (streamable-http, a2a,
both); stdio has no HTTP surface and skips the route.

The manifest builder is a pure function in adcp.server.discovery so
adopters can also publish a static manifest from CI. New optional
serve() kwargs base_url / specialisms / description populate the
manifest's url + specialism + description fields; defaults are correct
for local development but production should set base_url to the public
TLS-terminated origin.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…(PR #406 fix-pack)

Discovery manifest must publish https:// URLs per the AdCP schema's
``^https://`` pattern (loopback exception only). resolve_base_url now
raises at boot when a non-loopback bind has no base_url, or when an
explicit http:// base_url is paired with a non-loopback host — silent
mis-publication survives in CDNs and conformance reports for hours.

Also: strip separators before the 64-char agent_id cap so truncation
never lands on a stripped char, and quantize last_updated to whole-hour
granularity so consecutive requests within an hour are byte-identical
(HTTP caches collapse the duplicates).

Tests restore the ``^https://`` pattern in the inlined schema and add
coverage for the new boot-time errors.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@bokelley bokelley force-pushed the bokelley/issue-381-discovery-doc branch from defffb0 to 6773db7 Compare May 3, 2026 02:26
@bokelley bokelley merged commit e6dfaad into main May 3, 2026
11 of 12 checks passed
bokelley added a commit that referenced this pull request May 3, 2026
… worktrees (#433)

* fix(testing): make a2a_compat_shim resilient to wrong a2a-sdk in /tmp worktrees

Attribute assignments like `pb.Role.user = pb.Role.ROLE_USER` at module
import time would raise AttributeError if a2a-sdk isn't at the pinned
version (>=1.0.1,<1.0.2), propagating through conftest.py's top-level
import and breaking pytest collection entirely. Agents running in fresh
/tmp worktrees with uninitialized environments hit this on PRs #391, #406, #407.

Two changes:
- `a2a_compat_shim.py`: introduce `_proto_alias()` helper that guards each
  attribute alias independently with hasattr + a per-alias RuntimeWarning
  (includes install command) rather than letting AttributeError propagate.
- `conftest.py`: wrap the shim import in try/except (ImportError|AttributeError)
  with a fallback to None; update the autouse fixture to no-op when the shim
  is unavailable, so collection always succeeds and only A2A tests fail.

https://claude.ai/code/session_01AnL37fUet4e3yXt9YBxd7a

* fix(testing): stacklevel=2 in _proto_alias + document _STATE_STRING_MAP asymmetry

stacklevel=2 makes the per-alias warning point at the _proto_alias() call
site in the module body (the useful diagnostic location) rather than at
the warnings.warn() line inside the helper.

Add a comment at _STATE_STRING_MAP explaining that any AttributeError from
the dict literal is caught by conftest.py's import guard, so the different
guard pattern is intentional and collection still succeeds.

https://claude.ai/code/session_01AnL37fUet4e3yXt9YBxd7a

---------

Co-authored-by: Claude <noreply@anthropic.com>
bokelley added a commit that referenced this pull request May 3, 2026
…readiness flake (#435)

* perf(server): lazy-load Pydantic outputSchema generation to fix storyboard readiness flake

_generate_pydantic_schemas(), _generate_pydantic_output_schemas(), and
_apply_pydantic_schemas() previously ran at module import time, causing
heavy Pydantic type imports to race with the storyboard readiness probe
and producing "Agent unreachable" failures across PRs #391, #405, #406, #407.

Generation is now deferred to the first get_tools_for_handler() call (which
fires during create_mcp_tools() at server construction, not at import time).
_PYDANTIC_SCHEMAS and _PYDANTIC_OUTPUT_SCHEMAS start as empty dicts and are
populated via .update() so external references stay valid. The _schemas_applied
sentinel makes subsequent calls no-ops (~0ms overhead on the hot path).

Import-time delta: ~4.5s of schema generation is moved from `import adcp.server`
to the first `create_mcp_tools()` call.

Tests updated: conftest.py gains a session-scoped autouse fixture that triggers
lazy init before any test reads ADCP_TOOL_DEFINITIONS schema fields; stale
"at import time" references in docstrings and error messages are updated.

Closes #412

https://claude.ai/code/session_01NnoQN3c6Wi5LY5DEUBp8W2

* fixup: update stale 'at import time' docstrings and error messages

Addresses pre-PR review findings: test_spec_coverage.py assertion message
still referenced 'at import time', and _ensure_pydantic_schemas_applied
docstring understated the in-place mutation and misdirected to
get_tools_for_handler instead of create_mcp_tools.

https://claude.ai/code/session_01NnoQN3c6Wi5LY5DEUBp8W2

---------

Co-authored-by: Claude <noreply@anthropic.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.

1 participant