-
Notifications
You must be signed in to change notification settings - Fork 2.6k
MCP tool parameter type coercion missing: JSON strings not parsed before call_tool #3682
Description
Problem
When the LLM generates tool arguments for MCP tools with complex parameter types (arrays, booleans, numbers), they are emitted as JSON-encoded strings rather than native JSON values. hermes-agent passes these string values directly to session.call_tool() without parsing them first, causing validation errors on servers that expect typed values.
Errors observed (from logs)
# firecrawl/firecrawl_scrape
Tool 'firecrawl_scrape' parameter validation failed: formats: Invalid input: expected array, received string
onlyMainContent: Invalid input: expected boolean, received string
# firecrawl/firecrawl_search / firecrawl_map
limit: Invalid input: expected number, received string
# exa/crawling_exa
urls: Invalid input: expected array, received string
Root cause
In tools/mcp_tool.py, _make_tool_handler() (line 1048) passes args directly to session.call_tool():
result = await server.session.call_tool(tool_name, arguments=args)When the LLM generates e.g. {"urls": "[\"https://...\"]"} (a string), it arrives as a string. The MCP server's JSON-RPC layer then validates against the tool's inputSchema and rejects it.
Claude Code's MCP client implementation likely has type coercion after JSON-RPC parsing but before dispatch.
Proposed fix
Add JSON-string coercion in _make_tool_handler() before the call_tool() call:
def _coerce_json_strings(args: dict) -> dict:
\"\"\"Coerce string values that are valid JSON into native types.\"\"\"
for k, v in args.items():
if isinstance(v, str):
try:
parsed = json.loads(v)
# Only coerce if the result is a different type (e.g. list, bool, int)
# Don't coerce plain strings that happen to be valid JSON (e.g. \"foo\")
if isinstance(parsed, (list, dict)) or type(parsed) != str:
args[k] = parsed
except (json.JSONDecodeError, ValueError):
pass
return argsThen in the handler:
result = await server.session.call_tool(tool_name, arguments=_coerce_json_strings(args))Additional issue: gitmcp utility tools
The gitmcp server (gitmcp.io via mcp-remote) does not implement list_resources, list_prompts, read_resource, or get_prompt. hermes logs:
MCP gitmcp/list_resources failed: Method not found
MCP gitmcp/list_prompts failed: Method not found
These should be filtered out at discovery time rather than failing at call time. The _select_utility_schemas() function (line 1483) checks hasattr(server.session, required_method) but that check passes because the session object has the method — it just returns an error at the transport layer.
Workaround for gitmcp: disable utility tools in config:
gitmcp:
tools:
resources: false
prompts: falseBut ideally the discovery phase should also catch this, possibly by attempting a cheap probe call (e.g. list_resources() with a timeout) to verify the method actually works before registering the utility tool.
Environment
- hermes-agent: NousResearch/hermes-agent (latest main)
- mcp Python SDK: v1.26.0
- firecrawl server: StreamableHTTP transport
- gitmcp: gitmcp.io via npx mcp-remote