Skip to content

[Bug]: MCP tool calls fail with ClosedResourceError and empty error message #19417

@bechemeko

Description

@bechemeko

Bug Description

hermes-mcp-closedresource-bug-report.md

Bug Description

MCP tool calls via hermes mcp fail with ClosedResourceError: (note: empty message) even when hermes mcp test paperclip passes successfully and tools are discovered. The ClosedResourceError exception message is being swallowed somewhere in the error handling path, making debugging impossible.

Key symptom: The error message is literally the empty string — ClosedResourceError: with nothing after the colon.

Steps to Reproduce

  1. Configure an MCP server in ~/.hermes/config.yaml (e.g., paperclip pointing to a Python stdio MCP server)
  2. Start hermes gateway run
  3. Wait for the gateway to register the MCP tools
  4. Run hermes mcp test paperclip — this passes ✓
  5. Attempt to call any MCP tool via the agent — it fails with ClosedResourceError:

Expected Behavior

MCP tool calls should either succeed or return a meaningful error message with details about what went wrong (e.g., which resource was closed, why, and where in the call stack).

Actual Behavior

MCP call failed: ClosedResourceError: 

The exception has an empty message. Looking at the hermes-agent source in tools/mcp_tool.py:

  1. The exception is caught in _call_once()_run_on_mcp_loop(_call(), timeout=tool_timeout)
  2. The _run_on_mcp_loop function does future.result(timeout=wait_timeout)
  3. This raises the concurrent.futures.CancelledError wrapped exception (if timeout) or the raw exception
  4. The exception propagates to _call_once()'s except Exception as exc: handler at line 2066
  5. The error is logged at line 2088-2091: MCP tool paperclip/paperclip_health call failed: with the exception repr
  6. The error is sanitized and returned as JSON

The ClosedResourceError class from anyio is initialized with no message argument:

# anyio._core._exceptions
class ClosedResourceError(Exception):
    """Raised when trying to use a resource that has been closed."""

When raised without an argument (e.g., raise ClosedResourceError), str(e) returns "", giving the empty error string.

Root Cause Analysis

The ClosedResourceError is raised by anyio.streams.memory when the memory object stream is closed. In the MCP Python SDK (mcp/client/stdio/__init__.py), ClosedResourceError is caught silently in the stdout_reader and stdin_writer tasks:

except anyio.ClosedResourceError:  # pragma: no cover
    await anyio.lowlevel.checkpoint()

This means the stream being closed is expected in normal operation (when the subprocess exits). However, when combined with hermes-agent's event loop sharing model, the ClosedResourceError can propagate up unexpectedly.

The real issue is that hermes-agent's error handler at line 2093-2094:

"error": _sanitize_error(
    f"MCP call failed: {type(exc).__name__}: {exc}"
)

Uses f"{exc}" which calls str(exc). Since ClosedResourceError() has no message, this produces an empty string. The exception itself (its type and origin) is not included in the error output shown to the user.

Environment

  • OS: Linux (Proxmox home lab)
  • Hermes Agent: v0.12.0 (2026.4.30) — latest from origin/main
  • Python: 3.11.15 (hermes venv) / 3.13.5 (system)
  • MCP Python SDK: 1.27.0
  • anyio: 4.13.0
  • MCP Server: Custom Python stdio MCP server (paperclip_server.py)

Additional Context

Related MCP Python SDK issues:

Suggested Fix

  1. Better exception formatting in tools/mcp_tool.py line 2093-2095: Include repr(exc) or at minimum the exception module/class name when str(exc) is empty:
exc_str = str(exc) if str(exc) else repr(exc)
"error": _sanitize_error(f"MCP call failed: {type(exc).__name__}: {exc_str}")
  1. Also log the full traceback for ClosedResourceError specifically, since it often indicates a stream/session lifecycle issue that needs source-level debugging.

  2. Consider session reconnect logic for ClosedResourceError — if the read stream was closed, the subprocess may have crashed and should be restarted.

Steps to Reproduce

Steps to Reproduce

  1. Configure an MCP server in ~/.hermes/config.yaml (e.g., paperclip pointing to a Python stdio MCP server)
  2. Start hermes gateway run
  3. Wait for the gateway to register the MCP tools
  4. Run hermes mcp test paperclip — this passes ✓
  5. Attempt to call any MCP tool via the agent — it fails with ClosedResourceError:

Expected Behavior

Expected Behavior

MCP tool calls should either succeed or return a meaningful error message with details about what went wrong (e.g., which resource was closed, why, and where in the call stack).

Actual Behavior

Actual Behavior

MCP call failed: ClosedResourceError: 

The exception has an empty message. Looking at the hermes-agent source in tools/mcp_tool.py:

  1. The exception is caught in _call_once()_run_on_mcp_loop(_call(), timeout=tool_timeout)
  2. The _run_on_mcp_loop function does future.result(timeout=wait_timeout)
  3. This raises the concurrent.futures.CancelledError wrapped exception (if timeout) or the raw exception
  4. The exception propagates to _call_once()'s except Exception as exc: handler at line 2066
  5. The error is logged at line 2088-2091: MCP tool paperclip/paperclip_health call failed: with the exception repr
  6. The error is sanitized and returned as JSON

The ClosedResourceError class from anyio is initialized with no message argument:

# anyio._core._exceptions
class ClosedResourceError(Exception):
    """Raised when trying to use a resource that has been closed."""

When raised without an argument (e.g., raise ClosedResourceError), str(e) returns "", giving the empty error string.

Affected Component

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

Messaging Platform (if gateway-related)

Telegram

Debug Report

Report       https://paste.rs/QpNOr
  agent.log    https://paste.rs/fNZug
  gateway.log  https://paste.rs/CARNq

Operating System

Debian

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Medium — degraded but workaround existstool/mcpMCP client and OAuthtype/bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions