fix: sanitize tool schemas for llama.cpp backends; restore MCP in TUI (#15032)

Local llama.cpp servers (e.g. ggml-org/llama.cpp:full-cuda) fail the entire
request with HTTP 400 'Unable to generate parser for this template. ...
Unrecognized schema: "object"' when any tool schema contains shapes its
json-schema-to-grammar converter can't handle:

  * 'type': 'object' without 'properties'
  * bare string schema values ('additionalProperties: "object"')
  * 'type': ['X', 'null'] arrays (nullable form)

Cloud providers accept these silently, so they ship from external MCP
servers (Atlassian, GCloud, Datadog) and from a couple of our own tools.

Changes

- tools/schema_sanitizer.py: walks the finalized tool list right before it
  leaves get_tool_definitions() and repairs the hostile shapes in a deep
  copy. No-op on well-formed schemas. Recurses into properties, items,
  additionalProperties, anyOf/oneOf/allOf, and $defs.
- model_tools.get_tool_definitions(): invoke the sanitizer as the last
  step so all paths (built-in, MCP, plugin, dynamically-rebuilt) get
  covered uniformly.
- tools/browser_cdp_tool.py, tools/mcp_tool.py: fix our own bare-object
  schemas so sanitization isn't load-bearing for in-repo tools.
- tui_gateway/server.py: _load_enabled_toolsets() was passing
  include_default_mcp_servers=False at runtime. That's the config-editing
  variant (see PR #3252) — it silently drops every default MCP server
  from the TUI's enabled_toolsets, which is why the TUI didn't hit the
  llama.cpp crash (no MCP tools sent at all). Switch to True so TUI
  matches CLI behavior.

Tests

tests/tools/test_schema_sanitizer.py (17 tests) covers the individual
failure modes, well-formed pass-through, deep-copy isolation, and
required-field pruning.

E2E: loaded the default 'hermes-cli' toolset with MCP discovery and
confirmed all 27 resolved tool schemas pass a llama.cpp-compatibility
walk (no 'object' node missing 'properties', no bare-string schema
values).
This commit is contained in:
Teknium 2026-04-24 02:44:46 -07:00 committed by GitHub
parent 5dda4cab41
commit 34c3e67109
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 413 additions and 1 deletions

View file

@ -590,8 +590,14 @@ def _load_enabled_toolsets() -> list[str] | None:
from hermes_cli.config import load_config
from hermes_cli.tools_config import _get_platform_tools
# Runtime toolset resolution must include default MCP servers so the
# agent can actually call them. Passing ``False`` here is the
# config-editing variant — used when we need to persist a toolset
# list without baking in implicit MCP defaults. Using the wrong
# variant at agent creation time makes MCP tools silently missing
# from the TUI. See PR #3252 for the original design split.
enabled = sorted(
_get_platform_tools(load_config(), "cli", include_default_mcp_servers=False)
_get_platform_tools(load_config(), "cli", include_default_mcp_servers=True)
)
return enabled or None
except Exception: