OpenHands / OpenHands/software-agent-sdk
MCP connection failure aborts send_message before the LLM is called
Nobody has claimed this yet.
- 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
- 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.
- Optionally keep strict behaviour behind a flag for setups that genuinely require MCP.
- Include the server name and attempted URL in the
MCPErrormessage. - Suppress or chain the
sync_closeexception so the originalConnectErrorstays 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 _initialize → agent.py:367 init_state → local_conversation.py:703 _ensure_agent_ready → local_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
TimeoutErrorbranch increate_mcp_tools() - Strict fail-fast behaviour remains available behind an opt-in flag
- The
sync_close()teardown exception no longer masks the originalConnectErrorin 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
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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