Bug Description
When Hermes handles a server-initiated MCP sampling request with tools, the auxiliary LLM's tool-call arguments are parsed as JSON before Hermes builds an MCP ToolUseContent response.
If the arguments are syntactically valid JSON but the top-level value is not an object, such as [], null, 42, or true, Hermes passes a list or scalar into ToolUseContent.input. MCP SDK 2.0.0 requires that field to be a dictionary, so the sampling callback raises a Pydantic ValidationError and does not return the tool-use response to the MCP server.
Malformed JSON is already preserved as {"_raw": ...}. Valid non-object JSON falls through a gap in the same normalization path.
Steps to Reproduce
- Check out
main at 6e2b8e070d28b1a3381a3fb290b6b8d6cce13cef.
- Configure an MCP sampling callback with tools enabled.
- Have the auxiliary LLM return a tool call whose arguments string is valid non-object JSON, for example
[].
- Build the sampling tool-use response.
Minimal reproducer:
from mcp.types import ToolUseContent
from tools.mcp_tool_sampling import _parse_tool_call_arguments
raw = "[]"
ToolUseContent(
type="tool_use",
id="call_1",
name="some_tool",
input=_parse_tool_call_arguments("server", raw),
)
A focused callback-level regression test also reproduces all four inputs:
.venv/bin/pytest --confcutdir=tests/tools tests/tools/test_mcp_tool.py -q -k non_object_json_wrapped_as_raw
Result: four failures at tools/mcp_tool_sampling.py:153-155, each reporting that ToolUseContent.input must be a valid dictionary.
Expected Behavior
The sampling callback should always provide a dictionary to ToolUseContent.input. Valid JSON objects should remain objects. Valid non-object JSON should be preserved in a dictionary wrapper, consistent with the existing malformed-JSON behavior, so the MCP server receives a valid tool-use response.
Actual Behavior
The callback raises pydantic_core.ValidationError and fails before returning the MCP sampling response:
1 validation error for ToolUseContent
input
Input should be a valid dictionary
Affected Component
Tools (MCP sampling)
Messaging Platform
N/A
Debug Report
Not applicable. This is a deterministic source-level reproduction against current main; it does not depend on local Hermes configuration, credentials, or runtime logs.
Environment
- Operating system: macOS 26.5.2
- Python: 3.11.15
- Hermes: 0.21.1, commit
6e2b8e070d28b1a3381a3fb290b6b8d6cce13cef
- MCP SDK: 2.0.0, as pinned by the repository
Root Cause Analysis
tools/mcp_tool_sampling.py:74-83 returns json.loads(args) directly for any syntactically valid JSON string. It checks whether non-string inputs are dictionaries, but it does not apply the same check to the parsed result.
tools/mcp_tool_sampling.py:153-155 then passes that value directly to ToolUseContent.input, whose MCP SDK 2.0.0 contract is dict[str, Any].
Proposed Fix
Parse string arguments, then return the parsed value only when it is a dictionary. Preserve all malformed or valid non-object inputs as {"_raw": original_value}. Add callback-level regression coverage for arrays, null, numbers, and booleans while keeping the existing valid-object and malformed-JSON behavior unchanged.
PR Offer
Bug Description
When Hermes handles a server-initiated MCP sampling request with tools, the auxiliary LLM's tool-call
argumentsare parsed as JSON before Hermes builds an MCPToolUseContentresponse.If the arguments are syntactically valid JSON but the top-level value is not an object, such as
[],null,42, ortrue, Hermes passes a list or scalar intoToolUseContent.input. MCP SDK 2.0.0 requires that field to be a dictionary, so the sampling callback raises a PydanticValidationErrorand does not return the tool-use response to the MCP server.Malformed JSON is already preserved as
{"_raw": ...}. Valid non-object JSON falls through a gap in the same normalization path.Steps to Reproduce
mainat6e2b8e070d28b1a3381a3fb290b6b8d6cce13cef.[].Minimal reproducer:
A focused callback-level regression test also reproduces all four inputs:
Result: four failures at
tools/mcp_tool_sampling.py:153-155, each reporting thatToolUseContent.inputmust be a valid dictionary.Expected Behavior
The sampling callback should always provide a dictionary to
ToolUseContent.input. Valid JSON objects should remain objects. Valid non-object JSON should be preserved in a dictionary wrapper, consistent with the existing malformed-JSON behavior, so the MCP server receives a valid tool-use response.Actual Behavior
The callback raises
pydantic_core.ValidationErrorand fails before returning the MCP sampling response:Affected Component
Tools (MCP sampling)
Messaging Platform
N/A
Debug Report
Not applicable. This is a deterministic source-level reproduction against current
main; it does not depend on local Hermes configuration, credentials, or runtime logs.Environment
6e2b8e070d28b1a3381a3fb290b6b8d6cce13cefRoot Cause Analysis
tools/mcp_tool_sampling.py:74-83returnsjson.loads(args)directly for any syntactically valid JSON string. It checks whether non-string inputs are dictionaries, but it does not apply the same check to the parsed result.tools/mcp_tool_sampling.py:153-155then passes that value directly toToolUseContent.input, whose MCP SDK 2.0.0 contract isdict[str, Any].Proposed Fix
Parse string arguments, then return the parsed value only when it is a dictionary. Preserve all malformed or valid non-object inputs as
{"_raw": original_value}. Add callback-level regression coverage for arrays, null, numbers, and booleans while keeping the existing valid-object and malformed-JSON behavior unchanged.PR Offer