From f35eb1e51501471bf6c5673c4d42315dbbf445b6 Mon Sep 17 00:00:00 2001 From: openhands Date: Fri, 31 Jul 2026 11:34:43 +0000 Subject: [PATCH] fix(sdk): exclude boolean false from anyOf non-null type selection PR #4185 added boolean JSON Schema node handling but introduced a bug in the anyOf filter: the condition `not isinstance(t, dict)` treats `False` as a non-null type because `not isinstance(False, dict)` is `True`. This causes `_process_schema_node(False)` to return `{"not": {}}` (rejects everything), which then becomes the selected type for the parameter. When an MCP server's schema contains `anyOf: [false, {"type": "string"}]` (a pattern common in TypeScript-generated schemas where `false` represents an impossible branch), the processor picks `{"not": {}}` as the parameter type, making it impossible for the LLM to provide a valid value. Fix: change the filter to `isinstance(t, dict) and t.get("type") != "null"` so non-dict values (booleans True/False) are excluded from the non-null type list, and only actual schema objects with a non-null type are considered. Co-authored-by: openhands --- openhands-sdk/openhands/sdk/tool/schema.py | 4 +--- tests/sdk/tool/test_mcp_schema.py | 22 ++++++++++++++++++++++ 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/openhands-sdk/openhands/sdk/tool/schema.py b/openhands-sdk/openhands/sdk/tool/schema.py index e5d6033ecb..bfe3de1247 100644 --- a/openhands-sdk/openhands/sdk/tool/schema.py +++ b/openhands-sdk/openhands/sdk/tool/schema.py @@ -139,9 +139,7 @@ def _process_schema_node( # Handle anyOf (often used for optional fields with None) if "anyOf" in node: non_null_types = [ - t - for t in node["anyOf"] - if not isinstance(t, dict) or t.get("type") != "null" + t for t in node["anyOf"] if isinstance(t, dict) and t.get("type") != "null" ] if non_null_types: # Process the first non-null type diff --git a/tests/sdk/tool/test_mcp_schema.py b/tests/sdk/tool/test_mcp_schema.py index 05967fbb65..e31dc07089 100644 --- a/tests/sdk/tool/test_mcp_schema.py +++ b/tests/sdk/tool/test_mcp_schema.py @@ -432,3 +432,25 @@ def test_boolean_schema_nodes_are_normalized(node, expected): assert result == expected json.dumps(result) + + +def test_anyof_with_false_does_not_clobber_concrete_type(): + """A ``false`` member in ``anyOf`` must not become the representative type. + + JSON Schema ``false`` means "reject everything." When it appears as one + branch of an ``anyOf`` alongside a concrete type (common in schemas emitted + by TypeScript/MCP servers), the processor must pick the concrete type, not + ``false``. Previously the filter ``not isinstance(t, dict)`` treated + ``False`` as a non-null type, so ``_process_schema_node(False)`` returned + ``{"not": {}}`` — making the parameter impossible for the LLM to satisfy. + """ + schema = { + "type": "object", + "properties": { + "value": { + "anyOf": [False, {"type": "string"}], + }, + }, + } + result = _process_schema_node(schema, {}) + assert result["properties"]["value"] == {"type": "string"}