Skip to content

MCP tool parameter type coercion missing: JSON strings not parsed before call_tool #3682

@acsezen

Description

@acsezen

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 args

Then 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: false

But 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions