diff --git a/CHANGELOG.md b/CHANGELOG.md index a113d41..8af5986 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,18 @@ 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). +## [5.1.0] - 2026-03-19 + +### Added + +- New `langgraph` optional extra for MCP tool interception: `pip install 'axonflow[langgraph]'`. The `mcp` package is now an opt-in dependency rather than being imported unconditionally at the package level. + +### Fixed + +- `mcp_tool_interceptor()` now wraps redacted output in a `CallToolResult` instead of returning a plain `str`. Previously, when `mcp_check_output` applied redaction, the interceptor returned the redacted string directly, causing `AttributeError: 'str' object has no attribute 'content'` in `langchain-mcp-adapters`. + +--- + ## [5.0.0] - 2026-03-16 ### Breaking Changes diff --git a/axonflow/_version.py b/axonflow/_version.py index e8038eb..91284fe 100644 --- a/axonflow/_version.py +++ b/axonflow/_version.py @@ -1,3 +1,3 @@ """Single source of truth for the AxonFlow SDK version.""" -__version__ = "5.0.0" +__version__ = "5.1.0" diff --git a/axonflow/adapters/langgraph.py b/axonflow/adapters/langgraph.py index 5d25822..06456bd 100644 --- a/axonflow/adapters/langgraph.py +++ b/axonflow/adapters/langgraph.py @@ -538,6 +538,15 @@ def mcp_tool_interceptor( An async callable ``(request, handler) -> result`` suitable for ``MultiServerMCPClient(tool_interceptors=[...])``. """ + try: + from mcp.types import CallToolResult, TextContent # noqa: PLC0415 + except ImportError as exc: + msg = ( + "The 'mcp' package is required to use mcp_tool_interceptor. " + "Install it with: pip install 'axonflow[langgraph]'" + ) + raise ImportError(msg) from exc + opts = options or MCPInterceptorOptions() def _default_connector_type(request: Any) -> str: @@ -574,7 +583,9 @@ async def _interceptor(request: Any, handler: Callable[..., Any]) -> Any: output_check.block_reason or "Tool result blocked by policy" ) if output_check.redacted_data is not None: - return output_check.redacted_data + return CallToolResult( + content=[TextContent(type="text", text=output_check.redacted_data)] + ) return result diff --git a/pyproject.toml b/pyproject.toml index b1a7969..943297f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "axonflow" -version = "5.0.0" +version = "5.1.0" description = "AxonFlow Python SDK - Enterprise AI Governance in 3 Lines of Code" readme = "README.md" license = {text = "MIT"} @@ -52,6 +52,7 @@ dev = [ "black>=23.0.0", "isort>=5.12.0", "pre-commit>=3.0.0", + "mcp>=1.0.0", ] docs = [ "sphinx>=7.0.0", @@ -61,9 +62,11 @@ docs = [ ] openai = ["openai>=1.0.0"] anthropic = ["anthropic>=0.18.0"] +langgraph = ["mcp>=1.0.0"] all = [ "openai>=1.0.0", "anthropic>=0.18.0", + "mcp>=1.0.0", ] [project.urls] diff --git a/tests/test_langgraph_adapter.py b/tests/test_langgraph_adapter.py index efd0b59..69e54bf 100644 --- a/tests/test_langgraph_adapter.py +++ b/tests/test_langgraph_adapter.py @@ -7,6 +7,7 @@ from unittest.mock import AsyncMock, MagicMock import pytest +from mcp.types import CallToolResult, TextContent from axonflow import AxonFlow from axonflow.adapters.langgraph import AxonFlowLangGraphAdapter, MCPInterceptorOptions @@ -192,7 +193,10 @@ async def test_returns_redacted_data_when_present( result = await adapter.mcp_tool_interceptor()(MagicMock(), handler) - assert result == "[REDACTED]" + assert isinstance(result, CallToolResult) + assert len(result.content) == 1 + assert isinstance(result.content[0], TextContent) + assert result.content[0].text == "[REDACTED]" @pytest.mark.asyncio async def test_returns_original_result_when_no_redaction(