From 05487bc13cbfc2f1900edc3d662fcc00d28422d6 Mon Sep 17 00:00:00 2001 From: Nicole Gomes Date: Wed, 2 Sep 2026 13:19:57 -0300 Subject: [PATCH 1/3] fix: support langchain 1.3.x --- src/sap_cloud_sdk/agentgateway/converters.py | 26 ++++++++----- tests/agentgateway/unit/test_converters.py | 39 ++++++++++++++++++++ 2 files changed, 55 insertions(+), 10 deletions(-) diff --git a/src/sap_cloud_sdk/agentgateway/converters.py b/src/sap_cloud_sdk/agentgateway/converters.py index 18788c60..f11f8ad5 100644 --- a/src/sap_cloud_sdk/agentgateway/converters.py +++ b/src/sap_cloud_sdk/agentgateway/converters.py @@ -91,16 +91,6 @@ def mcp_tool_to_langchain( "Install it with: pip install sap-cloud-sdk[langchain]" ) from None - async def run(**kwargs) -> str: - resolved = ( - {k: v for k, v in kwargs.items() if v is not None} if omit_none else kwargs - ) - return await call_tool( - mcp_tool, - user_token=get_user_token, - **resolved, - ) - # Build args schema from input_schema properties = mcp_tool.input_schema.get("properties", {}) required = set(mcp_tool.input_schema.get("required", [])) @@ -114,6 +104,22 @@ async def run(**kwargs) -> str: fields[k] = (py_type, ...) args_schema = create_model(f"{mcp_tool.name}_args", **fields) if fields else None + # Closed over at conversion time: only keys declared in the MCP schema are + # forwarded, so any kwargs LangChain injects (config, run_manager, …) are + # silently ignored without needing an explicit deny-list. + allowed_keys = frozenset(properties) + + async def run(**kwargs) -> str: + tool_kwargs = {k: v for k, v in kwargs.items() if k in allowed_keys} + resolved = ( + {k: v for k, v in tool_kwargs.items() if v is not None} if omit_none else tool_kwargs + ) + return await call_tool( + mcp_tool, + user_token=get_user_token, + **resolved, + ) + return StructuredTool.from_function( coroutine=run, name=mcp_tool.name, diff --git a/tests/agentgateway/unit/test_converters.py b/tests/agentgateway/unit/test_converters.py index d6dce001..8f47b385 100644 --- a/tests/agentgateway/unit/test_converters.py +++ b/tests/agentgateway/unit/test_converters.py @@ -291,3 +291,42 @@ async def test_none_values_forwarded_when_omit_none_false(self): kwargs = call_tool.call_args.kwargs assert "showdeclinedreason" in kwargs assert kwargs["showdeclinedreason"] is None + + @pytest.mark.asyncio + async def test_langchain_config_kwarg_not_forwarded_to_call_tool(self): + """LangChain >= 0.3 injects 'config' into _arun; it must not reach call_tool.""" + call_tool = AsyncMock(return_value="ok") + lc_tool = mcp_tool_to_langchain(_make_tool(), call_tool, lambda: "token") + + await lc_tool.coroutine(eventid="E001", config={"configurable": {}}) + + assert "config" not in call_tool.call_args.kwargs + + @pytest.mark.asyncio + async def test_langchain_run_manager_kwarg_not_forwarded_to_call_tool(self): + """LangChain injects 'run_manager' into _arun; it must not reach call_tool.""" + call_tool = AsyncMock(return_value="ok") + lc_tool = mcp_tool_to_langchain(_make_tool(), call_tool, lambda: "token") + + await lc_tool.coroutine(eventid="E001", run_manager=object()) + + assert "run_manager" not in call_tool.call_args.kwargs + + @pytest.mark.asyncio + async def test_langchain_injected_kwargs_stripped_while_tool_params_pass_through(self): + """Both 'config' and 'run_manager' are stripped; real tool params still reach call_tool.""" + call_tool = AsyncMock(return_value="ok") + lc_tool = mcp_tool_to_langchain(_make_tool(), call_tool, lambda: "token") + + await lc_tool.coroutine( + eventid="E001", + showdeclinedreason="true", + config={"configurable": {}}, + run_manager=object(), + ) + + kwargs = call_tool.call_args.kwargs + assert kwargs["eventid"] == "E001" + assert kwargs["showdeclinedreason"] == "true" + assert "config" not in kwargs + assert "run_manager" not in kwargs From ed8ac73742f0b2695268844abac6ec8ff6dd09f9 Mon Sep 17 00:00:00 2001 From: Nicole Gomes Date: Wed, 2 Sep 2026 13:28:33 -0300 Subject: [PATCH 2/3] bump version --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index ce51441e..e40811eb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "sap-cloud-sdk" -version = "0.48.3" +version = "0.48.4" description = "SAP Cloud SDK for Python" readme = "README.md" license = "Apache-2.0" From 5b56d8b62c527ec70c8d6be1c118092d334ca902 Mon Sep 17 00:00:00 2001 From: Nicole Gomes Date: Wed, 2 Sep 2026 13:46:04 -0300 Subject: [PATCH 3/3] fix quality check --- src/sap_cloud_sdk/agentgateway/converters.py | 4 +++- tests/agentgateway/unit/test_converters.py | 3 +++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/sap_cloud_sdk/agentgateway/converters.py b/src/sap_cloud_sdk/agentgateway/converters.py index f11f8ad5..0ca54a23 100644 --- a/src/sap_cloud_sdk/agentgateway/converters.py +++ b/src/sap_cloud_sdk/agentgateway/converters.py @@ -112,7 +112,9 @@ def mcp_tool_to_langchain( async def run(**kwargs) -> str: tool_kwargs = {k: v for k, v in kwargs.items() if k in allowed_keys} resolved = ( - {k: v for k, v in tool_kwargs.items() if v is not None} if omit_none else tool_kwargs + {k: v for k, v in tool_kwargs.items() if v is not None} + if omit_none + else tool_kwargs ) return await call_tool( mcp_tool, diff --git a/tests/agentgateway/unit/test_converters.py b/tests/agentgateway/unit/test_converters.py index 8f47b385..5c3e08ee 100644 --- a/tests/agentgateway/unit/test_converters.py +++ b/tests/agentgateway/unit/test_converters.py @@ -298,6 +298,7 @@ async def test_langchain_config_kwarg_not_forwarded_to_call_tool(self): call_tool = AsyncMock(return_value="ok") lc_tool = mcp_tool_to_langchain(_make_tool(), call_tool, lambda: "token") + assert lc_tool.coroutine is not None await lc_tool.coroutine(eventid="E001", config={"configurable": {}}) assert "config" not in call_tool.call_args.kwargs @@ -308,6 +309,7 @@ async def test_langchain_run_manager_kwarg_not_forwarded_to_call_tool(self): call_tool = AsyncMock(return_value="ok") lc_tool = mcp_tool_to_langchain(_make_tool(), call_tool, lambda: "token") + assert lc_tool.coroutine is not None await lc_tool.coroutine(eventid="E001", run_manager=object()) assert "run_manager" not in call_tool.call_args.kwargs @@ -318,6 +320,7 @@ async def test_langchain_injected_kwargs_stripped_while_tool_params_pass_through call_tool = AsyncMock(return_value="ok") lc_tool = mcp_tool_to_langchain(_make_tool(), call_tool, lambda: "token") + assert lc_tool.coroutine is not None await lc_tool.coroutine( eventid="E001", showdeclinedreason="true",