#6299·odysseus

Stale stdio MCP sessions are never recovered for user-configured servers, and fail with an empty error

Author: JulianFriedleCreated Sep 14, 2026Updated Sep 14, 2026
Labelsready for review

Prerequisites

  • I searched open issues and discussions and did not find an existing report of this bug.
  • This is not a security vulnerability.
  • I am running the latest code from the dev branch and the bug still reproduces there.

Odysseus Revision

3b6c16916233 (2026-09-14)

Install Method

Docker (docker compose up)

Operating System

Linux

Steps to Reproduce

  1. With the app running, add a new stdio MCP server through Settings → Integrations → Add MCP Server. Any server works; a one-tool script is enough.
  2. The server connects: list_tools succeeds, the integration shows green, and the tool appears in the agent's tool selection.
  3. In agent mode, ask the model to use that tool.

Expected Behaviour

The tool call runs, or fails with a message that says what went wrong.

Actual Behaviour

The call fails, and the log line ends at the colon:

src.mcp_manager - ERROR - MCP tool call failed: mcp__ab545df6__send_message: 
src.tool_execution - INFO - Tool executed: mcp: mcp__ab545df6__send_message -> exit_code=1

Nothing after the colon. The same empty string is returned to the model as the tool result, so the model invents a cause — in my case it told me to check the API key of a server whose credentials were fine.

Restarting the app fixes it permanently: the same server, connected during startup instead of from a request, works on the first try.

Two separate defects

1. call_tool only reconnects builtin servers.

# src/mcp_manager.py
except Exception as e:
    # Auto-reconnect for builtin servers whose subprocess may have died
    if self.is_builtin(server_id):
        ...
    else:
        logger.error(f"MCP tool call failed: {qualified_name}: {e}")
        return {"error": str(e), "exit_code": 1}

The comment describes exactly the situation a user-configured server hits, but the branch excludes it. _reconnect_builtin has no counterpart for servers loaded from the McpServer table, even though _connect_with_timeout already connects them from the DB at startup.

2. str(e) is empty for the exception that actually occurs.

The session's streams are gone, so anyio.ClosedResourceError is raised, and str(anyio.ClosedResourceError()) is "". Reported verbatim it produces the bare colon above — in the log and in the tool result. This is what makes the bug expensive: there is nothing to search for, and the model fills the gap with a plausible-sounding but wrong explanation.

Root cause, as far as I can tell

stdio_client opens the subprocess inside whatever asyncio task is running. When a server is registered through the UI, that is the HTTP request task, which ends as soon as the response is sent; the streams then belong to a scope that no longer exists. Servers connected during startup live in a long-lived task and are unaffected — which matches the observed behaviour exactly.

Properly binding MCP sessions to a long-lived task is a larger change and I have not attempted it. The two defects above are what turn a recoverable condition into a permanent, undiagnosable failure.

Workaround

do_manage_mcp with action: "reconnect" recovers the server without a restart, so the capability is present — it just has to be invoked manually, which assumes you already know that a stale session is what you are looking at. With an empty error message, you do not.

Evidence

The same server, driven directly with the same SDK from inside the same container, works:

TOOLS: ['send_message']
ISERROR: False
CONTENT: Gesendet, message_id=3

Model / Backend (if relevant)

Ollama + qwen3:14b, but the failure is in the MCP layer and is independent of the model.

Are you willing to submit a fix?

Yes — I can open a PR

Additional Information

The empty message also reaches the model, so an agent will confidently report a wrong cause to the user. Even without the reconnect change, falling back to the exception class name would have saved a considerable amount of searching.