Summary
The official examples/seller_agent.py shipped with adcp 4.0.0 does not
pass the AdCP storyboard compliance runner out of the box. The MCP
streamable-http transport accepts the runner's initialize POST, returns
200 OK, then closes the response without completing — surfaced in uvicorn
as ASGI callable returned without completing response. The runner times out
after 120s and reports overall_status: "unreachable".
Reproduces on macOS 15 (arm64) and clean Ubuntu Linux (Fly.io x86_64).
Not environmental — contract mismatch between the SDK's serve() defaults
and what the runner expects.
Environment
Server (Python):
adcp 4.0.0 | mcp 1.27.0 | fastmcp 3.2.4 | uvicorn 0.46.0
anyio 4.13.0 | Python 3.13.1 (local), 3.11.9 (Fly Docker)
Runner (JS/TS):
@adcp/client 5.23.0 (also 5.13.0) | Node.js v24.9.0 (also v20)
Spec target: AdCP 3.0.1
OS: macOS 15 Darwin 25.2.0 arm64, Linux Fly.io Firecracker x86_64
Headline Finding
examples/seller_agent.py documents this validation command in its
module docstring (line 12):
# Validate:
# npx -y -p @adcp/client adcp storyboard run \
# http://localhost:3001/mcp media_buy_seller --json
This command does NOT pass — the agent reports unreachable.
Reproduction (5 minutes, no fork required)
git clone --depth 1 --branch v4.0.0 \
https://github.com/adcontextprotocol/adcp-client-python.git /tmp/adcp-sdk
python3 -m venv /tmp/adcp-sdk/.venv
/tmp/adcp-sdk/.venv/bin/pip install "adcp==4.0.0" mcp fastmcp
# Terminal A — start the SDK's own reference agent, unmodified
ADCP_PORT=3001 /tmp/adcp-sdk/.venv/bin/python /tmp/adcp-sdk/examples/seller_agent.py
# Terminal B — run the validation command from the example's docstring
npx -y -p @adcp/client adcp storyboard run \
http://localhost:3001/mcp media_buy_seller --json --allow-http
Actual Runner Output
{
"agent_url": "http://localhost:3001/mcp",
"agent_profile": { "name": "Unknown", "tools": [] },
"overall_status": "unreachable",
"summary": {
"tracks_passed": 0,
"tracks_failed": 0,
"tracks_skipped": 0,
"headline": "Agent unreachable — Failed to discover MCP endpoint. Tried:\n 1. http://localhost:3001/mcp\n 2. http://localhost:3001/mcp/\nNone responded to MCP protocol."
},
"total_duration_ms": 120044
}
Server Logs During Run
INFO: 127.0.0.1:50452 - "POST /mcp HTTP/1.1" 200 OK
ERROR: ASGI callable returned without completing response.
INFO: 127.0.0.1:50475 - "GET /mcp HTTP/1.1" 400 Bad Request
INFO: 127.0.0.1:50477 - "POST /mcp/ HTTP/1.1" 307 Temporary Redirect
INFO: 127.0.0.1:50475 - "POST /mcp HTTP/1.1" 200 OK
ERROR: ASGI callable returned without completing response.
Workaround
Patching FastMCP to use stateless_http=True and json_response=True
makes the response complete synchronously and the runner connects:
from mcp.server.fastmcp import FastMCP
_orig = FastMCP.__init__
def _patched(self, *args, **kwargs):
kwargs.setdefault("stateless_http", True)
kwargs.setdefault("json_response", True)
_orig(self, *args, **kwargs)
FastMCP.__init__ = _patched
After this patch the runner connects and starts running storyboards
(27/40 steps passing before hitting the secondary issues below).
Likely Root Cause
mcp 1.27.0's streaming-SSE response handler for streamable-http doesn't
flush/close on the runner's specific request shape. Fix surface is
adcp.server.serve() — defaulting stateless_http=True for
transport="streamable-http" is likely a one-line fix.
Secondary Findings
-
serve() doesn't pass host to FastMCP (src/adcp/server/serve.py:747)
FastMCP defaults to 127.0.0.1. Behind any reverse proxy (Fly, k8s,
Cloud Run) the container's external interface never receives traffic.
Recommend: expose host as a serve() kwarg with ADCP_HOST env fallback.
-
DNS-rebinding allowlist rejects production hostnames.
TransportSecuritySettings defaults allow only 127.0.0.1/localhost/[::1].
Any production hostname (e.g. myagent.fly.dev) gets 400.
Recommend: serve() accept transport_security kwarg or env-var defaults.
-
SCENARIOS whitelist out of sync with AdCP 3.0.1
(src/adcp/server/test_controller.py:48)
SDK declares 6 scenarios; runner schema (@adcp/client@5.23.0) declares 14,
including 6 seed_* scenarios used to pre-populate storyboard fixtures.
Without them, every storyboard with prerequisites.controller_seeding: true
fails preflight and cascades-skips.
Missing: force_create_media_buy_arm, force_task_completion, seed_product,
seed_pricing_option, seed_creative, seed_plan, seed_media_buy,
seed_creative_format.
-
Hardcoded if/elif dispatcher in _handle_test_controller returns
UNKNOWN_SCENARIO for anything beyond the 6 known branches.
Recommend: replace with getattr(store, scenario) dynamic dispatch.
-
capabilities_response() missing required adcp.idempotency field.
AdCP 3.0.1 schema marks this required. Without it the runner logs
validation failure and downgrades to v2 mode, cascading failures across
most tracks. Recommend: default to {"supported": False} when not provided.
-
comply_test_controller tool param schema strips runner's account field.
Runner sends { account, scenario, params }; SDK schema only declares
{ scenario, params, context }. Runner strips account, controller_detected
reports false even though the tool exists and list_scenarios succeeds.
Recommend: add account: {type: object} to the tool's inputSchema.
Impact
A new implementer following the SDK quickstart cannot pass storyboards
with the SDK's own example. Every issue above was discovered by reading
the runner's compiled JS source because the runner's error messages
provided no actionable detail. The fix surface for the headline is a
single line in adcp.server.serve().
Summary
The official
examples/seller_agent.pyshipped withadcp 4.0.0does notpass the AdCP storyboard compliance runner out of the box. The MCP
streamable-http transport accepts the runner's
initializePOST, returns200 OK, then closes the response without completing — surfaced in uvicornas
ASGI callable returned without completing response. The runner times outafter 120s and reports
overall_status: "unreachable".Reproduces on macOS 15 (arm64) and clean Ubuntu Linux (Fly.io x86_64).
Not environmental — contract mismatch between the SDK's
serve()defaultsand what the runner expects.
Environment
Server (Python):
adcp 4.0.0 | mcp 1.27.0 | fastmcp 3.2.4 | uvicorn 0.46.0
anyio 4.13.0 | Python 3.13.1 (local), 3.11.9 (Fly Docker)
Runner (JS/TS):
@adcp/client 5.23.0 (also 5.13.0) | Node.js v24.9.0 (also v20)
Spec target: AdCP 3.0.1
OS: macOS 15 Darwin 25.2.0 arm64, Linux Fly.io Firecracker x86_64
Headline Finding
examples/seller_agent.pydocuments this validation command in itsmodule docstring (line 12):
This command does NOT pass — the agent reports
unreachable.Reproduction (5 minutes, no fork required)
Actual Runner Output
Server Logs During Run
Workaround
Patching FastMCP to use stateless_http=True and json_response=True
makes the response complete synchronously and the runner connects:
After this patch the runner connects and starts running storyboards
(27/40 steps passing before hitting the secondary issues below).
Likely Root Cause
mcp 1.27.0's streaming-SSE response handler for streamable-http doesn't
flush/close on the runner's specific request shape. Fix surface is
adcp.server.serve() — defaulting stateless_http=True for
transport="streamable-http" is likely a one-line fix.
Secondary Findings
serve() doesn't pass host to FastMCP (src/adcp/server/serve.py:747)
FastMCP defaults to 127.0.0.1. Behind any reverse proxy (Fly, k8s,
Cloud Run) the container's external interface never receives traffic.
Recommend: expose host as a serve() kwarg with ADCP_HOST env fallback.
DNS-rebinding allowlist rejects production hostnames.
TransportSecuritySettings defaults allow only 127.0.0.1/localhost/[::1].
Any production hostname (e.g. myagent.fly.dev) gets 400.
Recommend: serve() accept transport_security kwarg or env-var defaults.
SCENARIOS whitelist out of sync with AdCP 3.0.1
(src/adcp/server/test_controller.py:48)
SDK declares 6 scenarios; runner schema (@adcp/client@5.23.0) declares 14,
including 6 seed_* scenarios used to pre-populate storyboard fixtures.
Without them, every storyboard with prerequisites.controller_seeding: true
fails preflight and cascades-skips.
Missing: force_create_media_buy_arm, force_task_completion, seed_product,
seed_pricing_option, seed_creative, seed_plan, seed_media_buy,
seed_creative_format.
Hardcoded if/elif dispatcher in _handle_test_controller returns
UNKNOWN_SCENARIO for anything beyond the 6 known branches.
Recommend: replace with getattr(store, scenario) dynamic dispatch.
capabilities_response() missing required adcp.idempotency field.
AdCP 3.0.1 schema marks this required. Without it the runner logs
validation failure and downgrades to v2 mode, cascading failures across
most tracks. Recommend: default to {"supported": False} when not provided.
comply_test_controller tool param schema strips runner's account field.
Runner sends { account, scenario, params }; SDK schema only declares
{ scenario, params, context }. Runner strips account, controller_detected
reports false even though the tool exists and list_scenarios succeeds.
Recommend: add account: {type: object} to the tool's inputSchema.
Impact
A new implementer following the SDK quickstart cannot pass storyboards
with the SDK's own example. Every issue above was discovered by reading
the runner's compiled JS source because the runner's error messages
provided no actionable detail. The fix surface for the headline is a
single line in adcp.server.serve().