OpenHands / OpenHands/software-agent-sdk

[Bug]: `anyOf: [false, X]` converts to `{"not": {}}`, clobbering the concrete branch

Open Beginner friendly
#5,091 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

bug mcp priority:medium ready-for-dev release-note-required tools
Dominant language
Python
Stars
1.1k
Forks
539
Avg merge
1d 19h
Merged PRs (30d)
137

Description

Is there an existing issue for the same bug?
  • I have searched existing issues and this is not a duplicate.
Bug Description

_process_schema_node in openhands-sdk/openhands/sdk/tool/schema.py picks the wrong anyOf member when a boolean false schema appears in the list. Its filter keeps every non-dict member:

non_null_types = [
    t
    for t in node["anyOf"]
    if not isinstance(t, dict) or t.get("type") != "null"
]

not isinstance(False, dict) is True, so false survives the filter, and only the first survivor is used. anyOf: [false, {"type": "string"}] therefore converts to {"not": {}} — a schema no value can satisfy — while the reversed anyOf: [{"type": "string"}, false] converts to {"type": "string"}. The conversion is order-dependent.

The same filter is the only thing standing between a malformed member (5, "string", None, [], 1.5) and an uncaught TypeError/AttributeError out of to_openai_tool(), which takes down the whole tool list rather than one tool.

This is reachable from untrusted input: MCPToolDefinition._get_tool_schema deep-copies the server-provided inputSchema and feeds it straight into _process_schema_node, so whatever an MCP server advertises reaches this code verbatim.

Expected Behavior

anyOf is a disjunction, so a false branch contributes nothing and the concrete branch should win regardless of position:

  • anyOf: [false, {"type": "string"}]{"type": "string"}
  • anyOf: [{"type": "string"}, false]{"type": "string"}

Degenerate unions with no viable branch must keep their reject-all meaning (anyOf: [false]{"not": {}}), and a member that is not a schema at all should be ignored rather than raising.

Actual Behavior

Running this against the current main with uv run python -c ... shows the order dependence:

uv run python -c "
from openhands.sdk.tool.schema import _process_schema_node
print(_process_schema_node({'anyOf': [False, {'type': 'string'}]}, {}))
print(_process_schema_node({'anyOf': [{'type': 'string'}, False]}, {}))
print(_process_schema_node({'anyOf': ['string', {'type': 'string'}]}, {}))
"

prints

{'not': {}}
{'type': 'string'}
AttributeError: 'str' object has no attribute 'get'

The first line should be {'type': 'string'} like the second, and the third case should not raise.

Steps to Reproduce
  1. uv sync in a checkout of this repository.
  2. Run the uv run python -c ... snippet above.
  3. Observe {'not': {}} for the [false, {"type": "string"}] union and an AttributeError for the non-schema member.
Acceptance Criteria
  • anyOf: [false, {"type": "string"}] and anyOf: [{"type": "string"}, false] both convert to {"type": "string"}.
  • anyOf: [false] and anyOf: [false, {"type": "null"}] still convert to {"not": {}}.
  • anyOf: [true, X] keeps converting to {} (unchanged behavior).
  • An anyOf member that is not a schema (5, "string", None, [], 1.5) is ignored instead of raising TypeError/AttributeError.
  • Regression tests covering the above live in tests/sdk/tool/test_mcp_schema.py and pass with uv run pytest tests/sdk/tool/test_mcp_schema.py -q.
Installation Method

Source checkout (uv sync)

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in openhands-sdk/openhands/sdk/tool/schema.py at _process_schema_node, then run the reproduction command from the issue. Add regression coverage in tests/sdk/tool/test_mcp_schema.py for false, true, null, and malformed anyOf members, and run uv run pytest tests/sdk/tool/test_mcp_schema.py -q to confirm the expected conversions and no exceptions.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
backend
Issue type
Bug
Difficulty
2/5
Estimated time
1-3 hours
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
86/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.