OpenHands / OpenHands/software-agent-sdk

MCP connection failure aborts send_message before the LLM is called

Open
#4,891 2 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

bug mcp priority:medium ready-for-dev release-note-required
Dominant language
Python
Stars
1.1k
Forks
539
Avg merge
1d 19h
Merged PRs (30d)
137

Description

Summary

When a configured MCP server is unreachable, create_mcp_tools() raises MCPError during agent initialization, which propagates all the way out of send_message(). The user's message never reaches the LLM and the conversation is dead. An optional tool source being unavailable should degrade, not abort.

Version

openhands-agent-server 1.26.0-python, run as a Docker sandbox behind the legacy Local GUI app server.

Call path
sockets.py:313 events_socket
  event_service.py:455 send_message
    local_conversation.py:888 send_message
      local_conversation.py:703 _ensure_agent_ready
        agent.py:367 init_state
          base.py:504 init_state
            base.py:531 _initialize
              mcp/utils.py:61 create_mcp_tools
                mcp/utils.py:36 _connect_and_list_tools
                  mcp/client.py:55 connect
openhands.sdk.mcp.exceptions.MCPError: MCP Connection Failure

Underlying cause: httpx.ConnectError: All connection attempts failed (the MCP base URL pointed at a host:port the sandbox container cannot reach).

Impact
  • Every prompt in the conversation fails. The UI shows only "MCP Connection Failure" or a generic "An error occurred".
  • The agent is otherwise fully healthy: WebSocket connected, 61 skills loaded, LLM profile saved, git tools working.
  • The failure is silent about which server failed and what URL it tried, which makes it hard to diagnose.
Secondary issue

In the traceback, mcp/client.py:97 sync_close raises during cleanup of the failed connection, so the teardown exception is chained onto the original. That makes the root cause harder to read than it needs to be.

Suggested behaviour
  1. Treat an unreachable MCP server as non-fatal by default: log a warning naming the server and the URL, drop its tools, and continue agent initialization.
  2. Optionally keep strict behaviour behind a flag for setups that genuinely require MCP.
  3. Include the server name and attempted URL in the MCPError message.
  4. Suppress or chain the sync_close exception so the original ConnectError stays the visible cause.
Actual Behavior

Reproducible directly against the SDK:

python -c "
from openhands.sdk.mcp.utils import create_mcp_tools
create_mcp_tools({'mcpServers': {'broken': {'url': 'http://127.0.0.1:59999/mcp/mcp'}}})
"

Raises openhands.sdk.mcp.exceptions.MCPError: MCP Connection Failure out of mcp/client.py:55 connect, caused by httpx.ConnectError: All connection attempts failed. The message names neither the server nor the URL.

In a real conversation the same exception propagates through base.py:531 _initializeagent.py:367 init_statelocal_conversation.py:703 _ensure_agent_readylocal_conversation.py:888 send_message, so the user's message never reaches the LLM and every subsequent prompt fails.

Note the asymmetry inside create_mcp_tools() itself: the except TimeoutError branch already lists the configured server names and prints troubleshooting guidance before raising MCPTimeoutError. Connection failures get no equivalent handling and propagate raw.

Relevant tests:

pytest tests/ -k mcp
Acceptance Criteria
  • An unreachable MCP server logs a warning naming the server and the attempted URL instead of raising by default
  • send_message() completes and the LLM is called when a configured MCP server is unreachable
  • Tools from reachable MCP servers are still registered when another server in the same config fails
  • Connection failures get the same server-name and guidance treatment as the existing TimeoutError branch in create_mcp_tools()
  • Strict fail-fast behaviour remains available behind an opt-in flag
  • The sync_close() teardown exception no longer masks the original ConnectError in the traceback
  • A test covers agent initialization with one reachable and one unreachable MCP server

Happy to open a PR if the maintainers agree on the approach.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with mcp/utils.py, especially create_mcp_tools() and its existing TimeoutError handling, then inspect mcp/client.py connect and sync_close. Run pytest tests/ -k mcp and reproduce the provided create_mcp_tools() command. Done means unreachable servers no longer abort initialization by default, reachable-server tools remain available, strict behavior is opt-in, and tests cover the mixed-server case and preserved connection cause.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
backend, tooling
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
55/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.