Conversation
…tected: false
FastMCP wraps a string-returning tool as structuredContent: {result: "<json>"},
but the JS runner's response unwrapper extracts that dict as completedData —
so data.success and data.scenarios are both undefined, and detectController()
returns {detected: false}.
Returning a dict causes FastMCP to put the fields directly in structuredContent,
so the unwrapper produces {success: true, scenarios: [...]} as completedData and
detection works correctly.
Closes #314
https://claude.ai/code/session_01LuThNVrmmHfVvPbfrbWsvy
…urn path Ensures the FastMCP registration path returns a dict (not a JSON string) from list_scenarios so the JS runner can read data.success/data.scenarios. Catches the structuredContent double-encoding regression from #314. https://claude.ai/code/session_01LuThNVrmmHfVvPbfrbWsvy
bokelley
added a commit
that referenced
this pull request
Apr 30, 2026
…) (#322) End-to-end against npx @adcp/client adcp storyboard run now reports overall_status: passing with 47/47 individual steps passing and controller_detected: true (was 36/47 partial after #317). Three fixes against examples/seller_agent.py: 1. Add four runner-fixture products to PRODUCTS — outdoor_display_q2, outdoor_video_q2, sports_preroll_q2, lifestyle_display_q2. The @adcp/client storyboard YAMLs reference these by ID without an explicit seed_product setup step; the seller is expected to know them out of the box. Pricing option IDs (cpm_standard, cpm_guaranteed) match what the compliance YAMLs send. 2. Validate measurement_terms in create_media_buy — reject with TERMS_REJECTED when max_variance_percent < 5 or measurement_window is not in (c3, c7). Source-of-truth is the measurement_terms_rejected.yaml storyboard which probes with c30 + 0% (rejected path) then retries with c7 + 10% (accepted path). Acceptance threshold matches the runner's relaxed shape. 3. Persist targeting_overlay (and friends) on packages — both create_media_buy and update_media_buy now preserve targeting_overlay, creative_assignments, creatives, and measurement_terms on the persisted package state, then surface them on get_media_buys. Storyboard inventory_list_targeting/verify_create_persisted round-trips property_list.list_id; the swap variant exercises the update parity check. Once this lands, the storyboard CI job (#309, currently continue-on-error: true) is promotable to required. Closes #319 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.
Closes #314
Root Cause
The JS storyboard runner (
@adcp/sdk) detects the test controller by callingcomply_test_controller(scenario='list_scenarios')and checking the response shape. ItsdetectControllerfunction:Previously,
comply_test_controllerreturnedjson.dumps(result)— a string. FastMCP wraps string-returning tools asstructuredContent: {result: "<json string>"}. The runner'sunwrapProtocolResponseextracts that asdata = {result: "<string>"}, sodata.successanddata.scenariosare bothundefined→controller_detected: false.Returning the result dict directly causes FastMCP to emit the fields at the top level (
structuredContent: {success: true, scenarios: [...]}or viacontent[0].text). Either path gives the runnerdata.success = trueanddata.scenarios = [...]→controller_detected: true.Confirmed locally: before the fix, the live MCP response was:
After the fix:
What Changed
src/adcp/server/test_controller.py:comply_test_controllerclosure inregister_test_controllerchanged from-> str+json.dumps(result)to-> dict[str, Any]returning the result directly. Thejsonimport is still live (used for the 256 KB payload size check in_handle_test_controller).tests/test_test_controller_context.py: removed thejson.loads()call in the one test that was callingtool.fndirectly; added a regression test that callsfn(scenario='list_scenarios')through the FastMCP registration path and asserts the result is adict(not a string).What Tested
pytest tests/test_test_controller_context.py tests/test_force_create_media_buy_arm_and_force_task_completion.py tests/test_server_dx.py— 75 passedpytest tests/(full suite, excluding pre-existing TLS conformance failure unrelated to this change) — 2312 passed, 0 new failuresruff check src/— passedtools/call comply_test_controller scenario=list_scenariosconfirmedstructuredContentnow contains the parsed dictPre-PR review:
Session: https://claude.ai/code/session_01LuThNVrmmHfVvPbfrbWsvy
Generated by Claude Code