From a8cd73bd7f7c4d38b024c000422ed0d7d93ef8b1 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 30 Apr 2026 03:04:48 +0000 Subject: [PATCH 1/2] fix(server): comply_test_controller returns dict to fix controller_detected: false MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit FastMCP wraps a string-returning tool as structuredContent: {result: ""}, 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 --- src/adcp/server/test_controller.py | 5 ++--- tests/test_test_controller_context.py | 5 +---- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/src/adcp/server/test_controller.py b/src/adcp/server/test_controller.py index 18c1ce720..9a2c4b535 100644 --- a/src/adcp/server/test_controller.py +++ b/src/adcp/server/test_controller.py @@ -683,7 +683,7 @@ async def force_account_status(self, account_id, status): from adcp.server.base import ToolContext as _ToolContext from adcp.server.serve import RequestMetadata as _RequestMetadata - async def comply_test_controller(**kwargs: Any) -> str: + async def comply_test_controller(**kwargs: Any) -> dict[str, Any]: context: _ToolContext | None = None if context_factory is not None: meta = _RequestMetadata(tool_name="comply_test_controller", transport="mcp") @@ -693,8 +693,7 @@ async def comply_test_controller(**kwargs: Any) -> str: "context_factory for comply_test_controller returned " f"{type(context).__name__}, not a ToolContext instance" ) - result = await _handle_test_controller(store, kwargs, context=context) - return json.dumps(result) + return await _handle_test_controller(store, kwargs, context=context) tool = Tool.from_function( comply_test_controller, diff --git a/tests/test_test_controller_context.py b/tests/test_test_controller_context.py index f45ba11f1..abd6e22ef 100644 --- a/tests/test_test_controller_context.py +++ b/tests/test_test_controller_context.py @@ -318,14 +318,11 @@ def build_context(meta: RequestMetadata) -> ToolContext: tool = mcp._tool_manager._tools["comply_test_controller"] # FastMCP's tool wrapper takes the function args as kwargs. fn = tool.fn # type: ignore[attr-defined] - result_json = await fn( + result = await fn( scenario="force_account_status", params={"account_id": "acc-1", "status": "suspended"}, ) - import json - - result = json.loads(result_json) assert result["success"] is True assert result["current_state"] == "suspended" # The factory ran, built a ToolContext, and the store saw the header- From 14842c7d274b1598ddacd11d09893df799845225 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 30 Apr 2026 03:08:50 +0000 Subject: [PATCH 2/2] test(server): add regression test for comply_test_controller dict return 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 --- tests/test_test_controller_context.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/tests/test_test_controller_context.py b/tests/test_test_controller_context.py index abd6e22ef..616e8468e 100644 --- a/tests/test_test_controller_context.py +++ b/tests/test_test_controller_context.py @@ -337,6 +337,27 @@ def build_context(meta: RequestMetadata) -> ToolContext: assert received[0].metadata["tool_name"] == "comply_test_controller" +async def test_register_test_controller_list_scenarios_returns_dict(): + """Regression for #314 — comply_test_controller must return a dict (not a + JSON string) through the FastMCP registration path so the JS runner's + structuredContent unwrapper can read data.success and data.scenarios.""" + + class _Store(TestControllerStore): + async def force_account_status(self, account_id: str, status: str) -> dict[str, Any]: + return {"previous_state": "active", "current_state": status} + + mcp = create_mcp_server(_MinimalHandler(), name="test-agent") + register_test_controller(mcp, _Store()) + + tool = mcp._tool_manager._tools["comply_test_controller"] + fn = tool.fn # type: ignore[attr-defined] + result = await fn(scenario="list_scenarios") + + assert isinstance(result, dict), "must be a dict, not a JSON string" + assert result["success"] is True + assert "force_account_status" in result["scenarios"] + + async def test_register_test_controller_rejects_non_toolcontext_from_factory(): """Guard rail — a factory that returns a dict instead of a ToolContext fails loudly at call time, not deep inside the store."""