From d21220767e7e13ba949a8bdd42174f5320fcfb00 Mon Sep 17 00:00:00 2001 From: Nicole Gomes Date: Thu, 18 Jun 2026 13:17:55 +0200 Subject: [PATCH 1/2] fix: optional mcp tools --- pyproject.toml | 2 +- src/sap_cloud_sdk/agentgateway/converters.py | 5 +++- tests/agentgateway/unit/test_converters.py | 25 ++++++++++++++++++++ 3 files changed, 30 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 491412ad..641483d7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "sap-cloud-sdk" -version = "0.27.0" +version = "0.27.1" description = "SAP Cloud SDK for Python" readme = "README.md" license = "Apache-2.0" diff --git a/src/sap_cloud_sdk/agentgateway/converters.py b/src/sap_cloud_sdk/agentgateway/converters.py index fa8f8587..84a4e12a 100644 --- a/src/sap_cloud_sdk/agentgateway/converters.py +++ b/src/sap_cloud_sdk/agentgateway/converters.py @@ -74,7 +74,10 @@ async def run(**kwargs) -> str: # Build args schema from input_schema properties = mcp_tool.input_schema.get("properties", {}) - fields: dict[str, Any] = {k: (str, ...) for k in properties} + required = set(mcp_tool.input_schema.get("required", [])) + fields: dict[str, Any] = { + k: (str, ...) if k in required else (str | None, None) for k in properties + } args_schema = create_model(f"{mcp_tool.name}_args", **fields) if fields else None return StructuredTool.from_function( diff --git a/tests/agentgateway/unit/test_converters.py b/tests/agentgateway/unit/test_converters.py index 21c7b020..4e28e387 100644 --- a/tests/agentgateway/unit/test_converters.py +++ b/tests/agentgateway/unit/test_converters.py @@ -78,6 +78,31 @@ def test_handles_empty_input_schema(self): assert result.name == "simple_tool" assert result.args_schema is not None + def test_optional_fields_not_required_in_args_schema(self): + """Fields absent from 'required' must be optional in the generated Pydantic model.""" + tool = MCPTool( + name="get_supplier_bid", + server_name="ariba", + description="Gets all supplier bids for the specified event", + input_schema={ + "type": "object", + "required": ["eventid"], + "properties": { + "eventid": {"description": "Unique identifier of the event"}, + "showdeclinedreason": {"description": "Show supplier decline reason"}, + "datafetchmode": {"description": "Level of detail for the response"}, + }, + }, + url="https://example.com/mcp", + ) + + result = mcp_tool_to_langchain(tool, AsyncMock(return_value="result"), lambda: "token") + + fields = result.args_schema.model_fields + assert fields["eventid"].is_required(), "eventid should be required" + assert not fields["showdeclinedreason"].is_required(), "showdeclinedreason should be optional" + assert not fields["datafetchmode"].is_required(), "datafetchmode should be optional" + def test_handles_input_schema_without_properties(self): """Handle MCPTool with input schema but no properties.""" tool = MCPTool( From f2fac476e49a89b8607663b3a415207acf035760 Mon Sep 17 00:00:00 2001 From: Nicole Gomes Date: Thu, 18 Jun 2026 13:30:25 +0200 Subject: [PATCH 2/2] fix: code quality --- tests/agentgateway/unit/test_converters.py | 3 +++ tests/core/integration/telemetry/_agent.py | 6 +++--- tests/core/integration/telemetry/test_telemetry_bdd.py | 2 +- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/tests/agentgateway/unit/test_converters.py b/tests/agentgateway/unit/test_converters.py index 4e28e387..0ac908fc 100644 --- a/tests/agentgateway/unit/test_converters.py +++ b/tests/agentgateway/unit/test_converters.py @@ -98,6 +98,9 @@ def test_optional_fields_not_required_in_args_schema(self): result = mcp_tool_to_langchain(tool, AsyncMock(return_value="result"), lambda: "token") + from pydantic import BaseModel + + assert result.args_schema is not None and isinstance(result.args_schema, type) and issubclass(result.args_schema, BaseModel) fields = result.args_schema.model_fields assert fields["eventid"].is_required(), "eventid should be required" assert not fields["showdeclinedreason"].is_required(), "showdeclinedreason should be optional" diff --git a/tests/core/integration/telemetry/_agent.py b/tests/core/integration/telemetry/_agent.py index b44ada81..a0d0343e 100644 --- a/tests/core/integration/telemetry/_agent.py +++ b/tests/core/integration/telemetry/_agent.py @@ -17,9 +17,9 @@ def build_langgraph_agent(): from typing import Annotated from langchain_core.messages import BaseMessage - from langchain_litellm import ChatLiteLLM # ty: ignore[unresolved-import] - from langgraph.graph import END, StateGraph # ty: ignore[unresolved-import] - from langgraph.graph.message import add_messages # ty: ignore[unresolved-import] + from langchain_litellm import ChatLiteLLM + from langgraph.graph import END, StateGraph + from langgraph.graph.message import add_messages except ImportError: pytest.skip("langchain-litellm or langgraph not installed") diff --git a/tests/core/integration/telemetry/test_telemetry_bdd.py b/tests/core/integration/telemetry/test_telemetry_bdd.py index d53e1464..aecbc017 100644 --- a/tests/core/integration/telemetry/test_telemetry_bdd.py +++ b/tests/core/integration/telemetry/test_telemetry_bdd.py @@ -127,7 +127,7 @@ def _llm_call(): Uses ChatLiteLLM so Traceloop's LangChain instrumentor fires and emits a span with gen_ai.usage.* token counts. """ - from langchain_litellm import ChatLiteLLM # ty: ignore[unresolved-import] + from langchain_litellm import ChatLiteLLM from langchain_core.messages import HumanMessage as LCHumanMessage model_name = os.environ.get("AICORE_MODEL", "anthropic--claude-4.5-sonnet") llm = ChatLiteLLM(model=f"sap/{model_name}")