Skip to content

adcp 4.0.0: examples/seller_agent.py fails its own storyboard validation command — streamable-http ASGI incomplete response #295

@tescoboy

Description

@tescoboy

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

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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.

  6. 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().

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions