hermes - ✅(Solved) Fix [Bug]: MCP HTTP client double-encodes anyOf:[string,array] arguments — Jina read_url always fails [2 pull requests, 2 comments, 1 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
NousResearch/hermes-agent#23129Fetched 2026-05-11 03:31:01
View on GitHub
Comments
2
Participants
1
Timeline
11
Reactions
0
Author
Participants
Timeline (top)
labeled ×4cross-referenced ×3commented ×2closed ×1

Error Message

The bug is silent (no error in Hermes logs at INFO level, only a [error] next to the tool call in the chat), and hard to diagnose without packet-level inspection. Observe: mcp_jina_read_url fails immediately (~0.4s) with [error]. All other Jina tools (search_web, search_arxiv, etc.) work fine on the same server. MCP error -32602: Input validation error: Invalid arguments for tool read_url: [ This is consistent with the failure mode: immediate (0.2-0.5s, no timeout), clean validation error from the server (not a 500 or generic failure).

Additional Logs / Traceback (optional)

Root Cause

When Hermes calls an MCP tool whose argument schema uses anyOf: [string, array] (or similar union types), the HTTP MCP client double-encodes the value: an array ["https://example.com"] is sent as the string literal "["https://example.com"]" (with brackets and quotes as part of the string), which the remote server then rejects. This breaks the official Jina MCP server's read_url tool. The 16 other Jina tools work because their parameters use plain type: string (no union). The bug is silent (no error in Hermes logs at INFO level, only a [error] next to the tool call in the chat), and hard to diagnose without packet-level inspection.

Fix Action

Fix / Workaround

Wrap bare non-list values when the schema declares array.

# Strings still go through _coerce_value first so JSON-encoded
# ...

prop_schema.get("type") returns None for anyOf/oneOf schemas (the type lives inside the variants). The coercion logic then takes the wrong branch. Patch attempted (does NOT fix) pythonexpected = prop_schema.get("type") if not expected: # Handle anyOf/oneOf schemas where type lives inside variants for union_key in ("anyOf", "oneOf"): variants = prop_schema.get(union_key) if variants: expected = [ v.get("type") for v in variants if isinstance(v, dict) and v.get("type") ] break Result: bug still present. The double-encoding happens elsewhere in the pipeline — probably upstream (parsing of Qwen's tool-call output from --jinja chat-template) or downstream (MCP HTTP request builder). The argument arrives at this coercion loop already malformed. Suggested investigation areas

Tool-call parsing of model output — where Hermes parses tool calls emitted by the LLM via llama.cpp's --jinja. Look for places where arguments is processed as a string and possibly json.dumps'd a second time. MCP HTTP tools/call request builder in tools/mcp_tool.py — check whether it calls json.dumps on values that are already JSON-serializable. anyOf/oneOf schema awareness in model_tools.py — the patch above is necessary but not sufficient.

Why only read_url Among the 17 Jina tools, read_url is one of the few whose primary parameter uses anyOf: [string, array]. The tools that work (search_web, etc.) likely receive a plain string from the model and take a different (working) coercion branch. This is consistent with the failure mode: immediate (0.2-0.5s, no timeout), clean validation error from the server (not a 500 or generic failure). Workarounds For users hitting this:

PR fix notes

PR #23213: fix(mcp): resolve anyOf/oneOf union types in coerce_tool_args

Description (problem / solution / changelog)

Summary

Fix coerce_tool_args() to resolve anyOf/oneOf union types when coercing MCP tool arguments. When a property schema uses a union type (e.g. {"anyOf": [{"type": "string"}, {"type": "array"}]}), the top-level "type" key is absent, causing coercion to be skipped entirely.

Root Cause

In model_tools.py, coerce_tool_args() reads expected = prop_schema.get("type") and skips coercion when it's None. MCP servers like Jina define parameters with anyOf/oneOf schemas instead of a direct type, so the coercion logic never fires.

This causes the HTTP MCP client to double-encode values: an array ["https://example.com"] is sent as the string literal "[\"https://example.com\"]", which the remote server rejects with Invalid url.

Fix

After the initial prop_schema.get("type") check, inspect anyOf/oneOf variants to extract concrete types. The extracted list is passed to _coerce_value(), which already handles list types by trying each variant in order.

Changed file: model_tools.py — 15 lines added to coerce_tool_args()

Regression Coverage

New test file tests/test_coerce_tool_args.py with 11 tests covering:

  • anyOf:[string, array] coerces JSON string to list (exact #23129 repro)
  • anyOf:[string, array] keeps plain strings as-is
  • oneOf:[string, integer] coerces numeric strings
  • Empty variants / no-type variants fall through correctly
  • Explicit type takes precedence over anyOf
  • Direct type coercion still works (regression guard)

Testing

All 11 new tests pass:

tests/test_coerce_tool_args.py — 11 passed in 2.80s

Fixes [Bug]: MCP HTTP client double-encodes anyOf:[string,array] arguments — Jina read_url always fails #23129

Changed files

  • gateway/platforms/whatsapp.py (modified, +4/-1)
  • model_tools.py (modified, +17/-1)
  • tests/test_coerce_tool_args.py (added, +176/-0)
  • tests/tools/test_file_tools.py (modified, +37/-0)
  • tools/file_tools.py (modified, +6/-6)

PR #23347: fix(agent): coerce tool args for anyOf/oneOf union schemas

Description (problem / solution / changelog)

MCP tools declaring parameters as anyOf: [{type: string}, {type: array}] were never coerced because prop_schema.get("type") returns None for union schemas. This adds anyOf/oneOf resolution so that string values are parsed as JSON arrays and bare scalars are wrapped in a list, preventing the double-encoding that breaks tools like Jina read_url.

Closes #23129

Changed files

  • model_tools.py (modified, +27/-2)
  • scripts/release.py (modified, +1/-0)

Code Example

Root cause hypothesis
The Jina read_url schema declares url as a union type (per Jina docs):
json{
  "url": {
    "anyOf": [
      {"type": "string", "format": "uri"},
      {"type": "array", "items": {"type": "string", "format": "uri"}}
    ]
  }
}
Jina's docs explicitly note: "Models like Qwen3-Next prefer calling singleton tools with multiple queries in an array. The singleton versions accept either a single string or an array of strings."
Qwen3.6 likely emits the URL as an array, which is valid per the schema. Hermes then double-encodes it on the way out.
Code investigation
In /opt/hermes/model_tools.py (around line 532-539), the argument-coercion loop:
pythonfor key, value in list(args.items()):
    prop_schema = properties.get(key)
    if not prop_schema:
        continue
    expected = prop_schema.get("type")

    # Wrap bare non-list values when the schema declares array.
    # Strings still go through _coerce_value first so JSON-encoded
    # ...
prop_schema.get("type") returns None for anyOf/oneOf schemas (the type lives inside the variants). The coercion logic then takes the wrong branch.
Patch attempted (does NOT fix)
pythonexpected = prop_schema.get("type")
if not expected:
    # Handle anyOf/oneOf schemas where type lives inside variants
    for union_key in ("anyOf", "oneOf"):
        variants = prop_schema.get(union_key)
        if variants:
            expected = [
                v.get("type")
                for v in variants
                if isinstance(v, dict) and v.get("type")
            ]
            break
Result: bug still present. The double-encoding happens elsewhere in the pipeline — probably upstream (parsing of Qwen's tool-call output from --jinja chat-template) or downstream (MCP HTTP request builder). The argument arrives at this coercion loop already malformed.
Suggested investigation areas

Tool-call parsing of model output — where Hermes parses tool calls emitted by the LLM via llama.cpp's --jinja. Look for places where arguments is processed as a string and possibly json.dumps'd a second time.
MCP HTTP tools/call request builder in tools/mcp_tool.py — check whether it calls json.dumps on values that are already JSON-serializable.
anyOf/oneOf schema awareness in model_tools.py — the patch above is necessary but not sufficient.

Why only read_url
Among the 17 Jina tools, read_url is one of the few whose primary parameter uses anyOf: [string, array]. The tools that work (search_web, etc.) likely receive a plain string from the model and take a different (working) coercion branch.
This is consistent with the failure mode: immediate (0.2-0.5s, no timeout), clean validation error from the server (not a 500 or generic failure).
Workarounds
For users hitting this:

Use mcporter via a custom skill that calls the same MCP server via npx mcporter call ... (mcporter's serialization is simpler and works).
Use Jina's HTTP Reader API directly (https://r.jina.ai/{url}) via a Python or bash skill, bypassing MCP.
Switch to a different web-extract provider (Tavily, Firecrawl) that uses Hermes's native web toolset rather than MCP.

Environment

Hermes Agent version: v0.13.0 (2026.5.7) — also reproduced on v0.11.0 (2026.4.23)
OS: Linux (Docker container, base image Debian)
Python: 3.13.5
OpenAI SDK: 2.24.0
Model: Qwen3.6-27B-UD-Q4_K_XL via llama.cpp b9050 with --jinja
Provider type: custom OpenAI-compatible endpoint (http://host.docker.internal:8080)
MCP server: Official Jina AI Remote MCP at https://mcp.jina.ai/v1, HTTP transport
Install method: Docker

Severity
Medium-high. Affects any MCP server with anyOf/oneOf argument schemas. Many community MCP servers use this pattern (especially "single value or list" parameters). Silent failure mode makes it hard to diagnose.
Related

Jina MCP server source & schemas: https://github.com/jina-ai/MCP
Hermes MCP config reference: https://hermes-agent.nousresearch.com/docs/reference/mcp-config-reference
Confirmed not fixed in v0.11 → v0.12 → v0.13 (PRs #19785 and #21204 fixed mcp add but not the serialization issue)

---
RAW_BUFFERClick to expand / collapse

Bug Description

When Hermes calls an MCP tool whose argument schema uses anyOf: [string, array] (or similar union types), the HTTP MCP client double-encodes the value: an array ["https://example.com"] is sent as the string literal "["https://example.com"]" (with brackets and quotes as part of the string), which the remote server then rejects. This breaks the official Jina MCP server's read_url tool. The 16 other Jina tools work because their parameters use plain type: string (no union). The bug is silent (no error in Hermes logs at INFO level, only a [error] next to the tool call in the chat), and hard to diagnose without packet-level inspection.

Steps to Reproduce

Steps to reproduce the behavior:

Configure the official Jina MCP server in ~/.hermes/config.yaml (or /opt/data/config.yaml in Docker):

yaml mcp_servers: jina: url: "https://mcp.jina.ai/v1?exclude_tags=parallel" headers: Authorization: "Bearer <JINA_API_KEY>" enabled: true timeout: 120

Verify connection succeeds:

bash hermes mcp test jina

✓ Connected (675ms)

✓ Tools discovered: 17

Start hermes chat and prompt:

lis https://example.com

Observe: mcp_jina_read_url fails immediately (~0.4s) with [error]. All other Jina tools (search_web, search_arxiv, etc.) work fine on the same server.

Expected Behavior

mcp_jina_read_url should fetch the page content and return it as markdown, as the same call does via curl or npx mcporter.

Actual Behavior

The Jina server returns: MCP error -32602: Input validation error: Invalid arguments for tool read_url: [ { "validation": "url", "code": "invalid_string", "message": "Invalid url", "path": ["url"] } ] Captured payload (DEBUG logs) The actual JSON-RPC request Hermes sends to https://mcp.jina.ai/v1: json{ "jsonrpc": "2.0", "id": 6, "method": "tools/call", "params": { "name": "read_url", "arguments": { "url": "["https://example.com"]" } } } Note: arguments.url is a string containing the literal text ["https://example.com"] (brackets and quotes are part of the string), not an array ["https://example.com"]. Proof the server and key are fine The same read_url call succeeds when made by other MCP clients with the same endpoint and key. Direct curl: bashcurl -X POST https://mcp.jina.ai/v1
-H "Authorization: Bearer <JINA_API_KEY>"
-H "Content-Type: application/json"
-H "Accept: application/json, text/event-stream"
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"read_url","arguments":{"url":"https://example.com"}}}'

→ returns clean markdown

mcporter: bashnpx mcporter call
--http-url "https://mcp.jina.ai/v1?exclude_tags=parallel"
--name jina
--header "Authorization=Bearer <JINA_API_KEY>"
read_url url=https://example.com
--output json

→ returns clean markdown

So the server, schema, and key are confirmed working. Only Hermes's MCP HTTP client mis-serializes.

Affected Component

Tools (terminal, file ops, web, code execution, etc.)

Messaging Platform (if gateway-related)

No response

Debug Report

Root cause hypothesis
The Jina read_url schema declares url as a union type (per Jina docs):
json{
  "url": {
    "anyOf": [
      {"type": "string", "format": "uri"},
      {"type": "array", "items": {"type": "string", "format": "uri"}}
    ]
  }
}
Jina's docs explicitly note: "Models like Qwen3-Next prefer calling singleton tools with multiple queries in an array. The singleton versions accept either a single string or an array of strings."
Qwen3.6 likely emits the URL as an array, which is valid per the schema. Hermes then double-encodes it on the way out.
Code investigation
In /opt/hermes/model_tools.py (around line 532-539), the argument-coercion loop:
pythonfor key, value in list(args.items()):
    prop_schema = properties.get(key)
    if not prop_schema:
        continue
    expected = prop_schema.get("type")

    # Wrap bare non-list values when the schema declares array.
    # Strings still go through _coerce_value first so JSON-encoded
    # ...
prop_schema.get("type") returns None for anyOf/oneOf schemas (the type lives inside the variants). The coercion logic then takes the wrong branch.
Patch attempted (does NOT fix)
pythonexpected = prop_schema.get("type")
if not expected:
    # Handle anyOf/oneOf schemas where type lives inside variants
    for union_key in ("anyOf", "oneOf"):
        variants = prop_schema.get(union_key)
        if variants:
            expected = [
                v.get("type")
                for v in variants
                if isinstance(v, dict) and v.get("type")
            ]
            break
Result: bug still present. The double-encoding happens elsewhere in the pipeline — probably upstream (parsing of Qwen's tool-call output from --jinja chat-template) or downstream (MCP HTTP request builder). The argument arrives at this coercion loop already malformed.
Suggested investigation areas

Tool-call parsing of model output — where Hermes parses tool calls emitted by the LLM via llama.cpp's --jinja. Look for places where arguments is processed as a string and possibly json.dumps'd a second time.
MCP HTTP tools/call request builder in tools/mcp_tool.py — check whether it calls json.dumps on values that are already JSON-serializable.
anyOf/oneOf schema awareness in model_tools.py — the patch above is necessary but not sufficient.

Why only read_url
Among the 17 Jina tools, read_url is one of the few whose primary parameter uses anyOf: [string, array]. The tools that work (search_web, etc.) likely receive a plain string from the model and take a different (working) coercion branch.
This is consistent with the failure mode: immediate (0.2-0.5s, no timeout), clean validation error from the server (not a 500 or generic failure).
Workarounds
For users hitting this:

Use mcporter via a custom skill that calls the same MCP server via npx mcporter call ... (mcporter's serialization is simpler and works).
Use Jina's HTTP Reader API directly (https://r.jina.ai/{url}) via a Python or bash skill, bypassing MCP.
Switch to a different web-extract provider (Tavily, Firecrawl) that uses Hermes's native web toolset rather than MCP.

Environment

Hermes Agent version: v0.13.0 (2026.5.7) — also reproduced on v0.11.0 (2026.4.23)
OS: Linux (Docker container, base image Debian)
Python: 3.13.5
OpenAI SDK: 2.24.0
Model: Qwen3.6-27B-UD-Q4_K_XL via llama.cpp b9050 with --jinja
Provider type: custom OpenAI-compatible endpoint (http://host.docker.internal:8080)
MCP server: Official Jina AI Remote MCP at https://mcp.jina.ai/v1, HTTP transport
Install method: Docker

Severity
Medium-high. Affects any MCP server with anyOf/oneOf argument schemas. Many community MCP servers use this pattern (especially "single value or list" parameters). Silent failure mode makes it hard to diagnose.
Related

Jina MCP server source & schemas: https://github.com/jina-ai/MCP
Hermes MCP config reference: https://hermes-agent.nousresearch.com/docs/reference/mcp-config-reference
Confirmed not fixed in v0.11 → v0.12 → v0.13 (PRs #19785 and #21204 fixed mcp add but not the serialization issue)

Operating System

MacOS

Python Version

No response

Hermes Version

No response

Additional Logs / Traceback (optional)

Root Cause Analysis (optional)

No response

Proposed Fix (optional)

No response

Are you willing to submit a PR for this?

  • I'd like to fix this myself and submit a PR

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

hermes - ✅(Solved) Fix [Bug]: MCP HTTP client double-encodes anyOf:[string,array] arguments — Jina read_url always fails [2 pull requests, 2 comments, 1 participants]