Skip to content

Commit fb06bc8

Browse files
committed
fix: support list of types
1 parent 210bcbb commit fb06bc8

2 files changed

Lines changed: 67 additions & 11 deletions

File tree

src/sap_cloud_sdk/agentgateway/converters.py

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,19 @@
2626
}
2727

2828

29+
def _resolve_type(json_type: Any) -> tuple[type, bool]:
30+
"""Return (python_type, is_nullable) from a JSON Schema ``type`` value.
31+
32+
Handles both the plain-string form (``"integer"``) and the array form
33+
(``["integer", "null"]``). Unknown or missing types map to ``Any``.
34+
"""
35+
if isinstance(json_type, list):
36+
nullable = "null" in json_type
37+
scalar = next((t for t in json_type if t != "null"), None)
38+
return _JSON_TYPE_MAP.get(scalar, Any), nullable
39+
return _JSON_TYPE_MAP.get(json_type, Any), False
40+
41+
2942
def mcp_tool_to_langchain(
3043
mcp_tool: MCPTool,
3144
call_tool: Callable,
@@ -91,17 +104,14 @@ async def run(**kwargs) -> str:
91104
# Build args schema from input_schema
92105
properties = mcp_tool.input_schema.get("properties", {})
93106
required = set(mcp_tool.input_schema.get("required", []))
94-
fields: dict[str, Any] = {
95-
k: (
96-
(_JSON_TYPE_MAP.get(v.get("type", ""), Any), ...)
97-
if k in required
98-
else (
99-
_JSON_TYPE_MAP.get(v.get("type", ""), Any) | None,
100-
Field(default=None),
101-
)
102-
)
103-
for k, v in properties.items()
104-
}
107+
fields: dict[str, Any] = {}
108+
for k, v in properties.items():
109+
py_type, type_nullable = _resolve_type(v.get("type"))
110+
optional = k not in required
111+
if optional or type_nullable:
112+
fields[k] = (py_type | None, Field(default=None))
113+
else:
114+
fields[k] = (py_type, ...)
105115
args_schema = create_model(f"{mcp_tool.name}_args", **fields) if fields else None
106116

107117
return StructuredTool.from_function(

tests/agentgateway/unit/test_converters.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,52 @@ def test_optional_non_string_field_is_nullable(self):
185185
assert int in field.annotation.__args__
186186
assert type(None) in field.annotation.__args__
187187

188+
def test_array_type_integer_null_maps_to_int(self):
189+
lc_tool = mcp_tool_to_langchain(
190+
self._tool_with_types({"limit": {"type": ["integer", "null"]}}, required=["limit"]),
191+
AsyncMock(),
192+
lambda: "token",
193+
)
194+
field = _schema_fields(lc_tool)["limit"]
195+
import types as _types
196+
assert isinstance(field.annotation, _types.UnionType)
197+
assert int in field.annotation.__args__
198+
assert type(None) in field.annotation.__args__
199+
200+
def test_array_type_number_null_maps_to_float(self):
201+
lc_tool = mcp_tool_to_langchain(
202+
self._tool_with_types({"ratio": {"type": ["number", "null"]}}, required=["ratio"]),
203+
AsyncMock(),
204+
lambda: "token",
205+
)
206+
field = _schema_fields(lc_tool)["ratio"]
207+
import types as _types
208+
assert isinstance(field.annotation, _types.UnionType)
209+
assert float in field.annotation.__args__
210+
assert type(None) in field.annotation.__args__
211+
212+
def test_array_type_multiple_scalars_uses_first_non_null(self):
213+
# e.g. {"type": ["number", "string", "null"]} — pick "number"
214+
lc_tool = mcp_tool_to_langchain(
215+
self._tool_with_types({"val": {"type": ["number", "string", "null"]}}, required=["val"]),
216+
AsyncMock(),
217+
lambda: "token",
218+
)
219+
field = _schema_fields(lc_tool)["val"]
220+
import types as _types
221+
assert isinstance(field.annotation, _types.UnionType)
222+
assert float in field.annotation.__args__
223+
assert type(None) in field.annotation.__args__
224+
225+
def test_array_type_without_null_is_not_nullable(self):
226+
lc_tool = mcp_tool_to_langchain(
227+
self._tool_with_types({"count": {"type": ["integer"]}}, required=["count"]),
228+
AsyncMock(),
229+
lambda: "token",
230+
)
231+
field = _schema_fields(lc_tool)["count"]
232+
assert field.annotation is int
233+
188234

189235
class TestMcpToolToLangchainInvocation:
190236
"""End-to-end invocation tests: verify what actually reaches call_tool."""

0 commit comments

Comments
 (0)