diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e124d9..3bb5a53 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ All notable changes to the AxonFlow Python SDK will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [3.9.0] - 2026-03-05 +## [3.9.0] - 2026-03-06 ### Added @@ -15,6 +15,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `operation`: Operation type forwarded to `mcp_check_input` (default: `"execute"`; use `"query"` for known read-only tool calls) - `MCPInterceptorOptions` and `WorkflowApprovalRequiredError` are now exported from `axonflow.adapters` +### Changed + +- `mcp_check_input()` default `operation` changed from `"query"` to `"execute"` to better reflect the default MCP tool call pattern where side effects are unknown + +### Fixed + +- `mcp_tool_interceptor()` now uses JSON serialization (`json.dumps`) for `statement` and output `message` fields instead of Python `repr()`, ensuring the policy engine receives valid structured data + --- ## [3.8.0] - 2026-03-03 diff --git a/axonflow/adapters/langgraph.py b/axonflow/adapters/langgraph.py index 3522dd0..0f111d4 100644 --- a/axonflow/adapters/langgraph.py +++ b/axonflow/adapters/langgraph.py @@ -33,6 +33,7 @@ from __future__ import annotations import asyncio +import json from dataclasses import dataclass, field from typing import TYPE_CHECKING, Any, Callable @@ -549,7 +550,8 @@ def _default_connector_type(request: Any) -> str: async def _interceptor(request: Any, handler: Callable[..., Any]) -> Any: connector_type = resolve_connector_type(request) - statement = f"{connector_type}({request.args!r})" + args_str = json.dumps(request.args, default=str) if request.args else "{}" + statement = f"{connector_type}({args_str})" pre_check = await self.client.mcp_check_input( connector_type=connector_type, @@ -562,9 +564,13 @@ async def _interceptor(request: Any, handler: Callable[..., Any]) -> Any: result = await handler(request) + try: + result_str = json.dumps(result, default=str) + except (TypeError, ValueError): + result_str = str(result) output_check = await self.client.mcp_check_output( connector_type=connector_type, - message=f"{{result: {result!r}}}", + message=result_str, ) if not output_check.allowed: raise PolicyViolationError( diff --git a/tests/test_langgraph_adapter.py b/tests/test_langgraph_adapter.py index 3e1929f..efd0b59 100644 --- a/tests/test_langgraph_adapter.py +++ b/tests/test_langgraph_adapter.py @@ -2,6 +2,7 @@ from __future__ import annotations +import json from typing import Any from unittest.mock import AsyncMock, MagicMock @@ -87,6 +88,9 @@ async def test_connector_type_derived_from_request( call_kwargs = client.mcp_check_input.call_args.kwargs assert call_kwargs["connector_type"] == "srv.tool" assert call_kwargs["parameters"] == request.args + # Statement uses JSON serialization, not Python repr + expected_args = json.dumps(request.args, default=str) + assert call_kwargs["statement"] == f"srv.tool({expected_args})" @pytest.mark.asyncio async def test_same_connector_type_sent_to_check_output( @@ -100,6 +104,18 @@ async def test_same_connector_type_sent_to_check_output( call_kwargs = client.mcp_check_output.call_args.kwargs assert call_kwargs["connector_type"] == "srv.tool" + @pytest.mark.asyncio + async def test_output_message_uses_json_serialization( + self, adapter: AxonFlowLangGraphAdapter, client: AxonFlow + ) -> None: + result_data = {"rows": [{"id": 1, "name": "test"}]} + handler = AsyncMock(return_value=result_data) + + await adapter.mcp_tool_interceptor()(_make_request(), handler) + + call_kwargs = client.mcp_check_output.call_args.kwargs + assert call_kwargs["message"] == json.dumps(result_data, default=str) + # --- operation --- @pytest.mark.asyncio