From 4b3065c533ccd3bdca0835bea0fdc6182352bd92 Mon Sep 17 00:00:00 2001 From: Saurabh Jain Date: Sat, 7 Mar 2026 23:24:41 +0530 Subject: [PATCH 1/4] fix(adapters): use JSON serialization in MCP interceptor The mcp_tool_interceptor was using Python repr() for statement and output message construction, producing non-standard formats like connector({"key": "value"}) and {result: 'data'}. This could cause false positives in SQLi detection and reduces policy scan quality. Changed to json.dumps(default=str) for both: - Input statement: connector_type(json_args) instead of repr - Output message: JSON-serialized result instead of ad-hoc format Added tests for statement and output message serialization format. --- axonflow/adapters/langgraph.py | 10 ++++++++-- tests/test_langgraph_adapter.py | 16 ++++++++++++++++ 2 files changed, 24 insertions(+), 2 deletions(-) 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 From b5eb3d571c602128fb187e9bc9aad62769857f0c Mon Sep 17 00:00:00 2001 From: Saurabh Jain Date: Sun, 8 Mar 2026 00:17:24 +0530 Subject: [PATCH 2/4] docs(changelog): add missing v3.9.0 entries for operation default and serialization fix --- CHANGELOG.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) 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 From 368726a947b85c0e1b8a594f912fd7509fe76b02 Mon Sep 17 00:00:00 2001 From: Saurabh Jain Date: Sun, 8 Mar 2026 00:20:10 +0530 Subject: [PATCH 3/4] docs(changelog): remove redundant Changed entry for operation default The operation default was changed before any release shipped with "query", so it is not a user-visible change. The Added section already documents the default as "execute". --- CHANGELOG.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3bb5a53..0ce5cbd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,10 +15,6 @@ 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 From 3dfc26697ea96b12c63a746f23c442d0af084db8 Mon Sep 17 00:00:00 2001 From: Saurabh Jain Date: Sun, 8 Mar 2026 00:22:47 +0530 Subject: [PATCH 4/4] docs(changelog): restore Changed entry for mcp_check_input operation default mcp_check_input() shipped in v3.7.0 with default "query". PR #106 changed it to "execute" after v3.8.0 was tagged. Users on v3.7.0/v3.8.0 saw "query" as the default, so this is a real behavioral change in v3.9.0. --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ce5cbd..3bb5a53 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,10 @@ 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