[PR #310] fix: reconnect when server invalidates the MCP session (404) #251

Open
opened 2026-10-18 21:13:00 +00:00 by bsakaguchi · 0 comments
Owner

📋 Pull Request Information

Original PR: https://github.com/open-webui/mcpo/pull/310
Author: @cdecarlos
Created: 9/4/2026
Status: 🔄 Open

Base: dev ← Head: fix/reconnect-on-session-terminated


📝 Commits (1)

  • 8b7ccae fix: reconnect when server invalidates the MCP session (404)

📊 Changes

2 files changed (+119 additions, -2 deletions)

View changed files

➕ src/mcpo/tests/test_reconnect.py (+96 -0)
📝 src/mcpo/utils/main.py (+23 -2)

📄 Description

Problem

call_tool_with_reconnect() only retries on ClosedResourceError (a transport-level failure). When the upstream MCP server invalidates a session server-side — TTL expiry on a FileSessionStore-backed server, a restart, etc. — and returns a 404, that surfaces as a hard error to the mcpo client instead of transparently reconnecting, even though MCPConnectionManager.reconnect() already exists and handles the transport-failure case fine.

Real-world traceback we hit in production:

mcp.shared.exceptions.McpError: Session terminated

which mcpo currently turns into an HTTP error response instead of retrying.

Root cause

python-sdk's StreamableHTTPTransport synthesizes McpError(code=32600, message="Session terminated") on any HTTP 404 response to a JSON-RPC request, discarding whatever error body the server actually sent (see _send_session_terminated_error in mcp/client/streamable_http.py, called unconditionally from _handle_post_request on status_code == 404).

One subtlety worth flagging explicitly: that 32600 is a bare positive literal hardcoded by the SDK — it is not mcp.types.INVALID_REQUEST (-32600, the JSON-RPC-spec-correct signed value). Matching against the typed constant would silently never fire; the fix matches the literal value the SDK actually emits, with a comment explaining why.

Fix

Catch that specific McpError (matched on code == 32600 and message == "Session terminated") alongside ClosedResourceError in call_tool_with_reconnect()'s retry path, and retry once against a freshly reconnected session. Any other McpError — a real tool-level or protocol error — still propagates unchanged.

The ensure_initialized() call path (before any session exists) does not need the same treatment: the synthesized error can only occur on a request carrying an already-established session_id.

Testing

  • Added src/mcpo/tests/test_reconnect.py: positive case (synthesized session-terminated error triggers exactly one reconnect + successful retry) and two negative cases (an unrelated McpError, and — to guard against the code-sign mixup above recurring — a real INVALID_REQUEST (-32600) error with a different message) confirm reconnection is not triggered for either.
  • Full suite passes (30/30).
  • Verified end-to-end against a local streamable-http MCP server (PHP, mcp/sdk-based, FileSessionStore): built mcpo from this branch, established a session, deleted the session's backing file to force the server to 404 on the next call, and confirmed the unpatched code returns a 500 to the mcpo client while this patch transparently negotiates a new session and completes the call.

Assisted with Claude Code


🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.

## 📋 Pull Request Information **Original PR:** https://github.com/open-webui/mcpo/pull/310 **Author:** [@cdecarlos](https://github.com/cdecarlos) **Created:** 9/4/2026 **Status:** 🔄 Open **Base:** `dev` ← **Head:** `fix/reconnect-on-session-terminated` --- ### 📝 Commits (1) - [`8b7ccae`](https://github.com/open-webui/mcpo/commit/8b7ccaeed7da5b2812a1a3babadd131770c5fb0e) fix: reconnect when server invalidates the MCP session (404) ### 📊 Changes **2 files changed** (+119 additions, -2 deletions) <details> <summary>View changed files</summary> ➕ `src/mcpo/tests/test_reconnect.py` (+96 -0) 📝 `src/mcpo/utils/main.py` (+23 -2) </details> ### 📄 Description ## Problem `call_tool_with_reconnect()` only retries on `ClosedResourceError` (a transport-level failure). When the upstream MCP server invalidates a session server-side — TTL expiry on a `FileSessionStore`-backed server, a restart, etc. — and returns a 404, that surfaces as a hard error to the mcpo client instead of transparently reconnecting, even though `MCPConnectionManager.reconnect()` already exists and handles the transport-failure case fine. Real-world traceback we hit in production: ``` mcp.shared.exceptions.McpError: Session terminated ``` which mcpo currently turns into an HTTP error response instead of retrying. ## Root cause python-sdk's `StreamableHTTPTransport` synthesizes `McpError(code=32600, message="Session terminated")` on **any** HTTP 404 response to a JSON-RPC request, discarding whatever error body the server actually sent (see [`_send_session_terminated_error`](https://github.com/modelcontextprotocol/python-sdk/blob/main/src/mcp/client/streamable_http.py) in `mcp/client/streamable_http.py`, called unconditionally from `_handle_post_request` on `status_code == 404`). One subtlety worth flagging explicitly: that `32600` is a bare **positive** literal hardcoded by the SDK — it is *not* `mcp.types.INVALID_REQUEST` (`-32600`, the JSON-RPC-spec-correct signed value). Matching against the typed constant would silently never fire; the fix matches the literal value the SDK actually emits, with a comment explaining why. ## Fix Catch that specific `McpError` (matched on `code == 32600 and message == "Session terminated"`) alongside `ClosedResourceError` in `call_tool_with_reconnect()`'s retry path, and retry once against a freshly reconnected session. Any other `McpError` — a real tool-level or protocol error — still propagates unchanged. The `ensure_initialized()` call path (before any session exists) does not need the same treatment: the synthesized error can only occur on a request carrying an already-established `session_id`. ## Testing - Added `src/mcpo/tests/test_reconnect.py`: positive case (synthesized session-terminated error triggers exactly one reconnect + successful retry) and two negative cases (an unrelated `McpError`, and — to guard against the code-sign mixup above recurring — a *real* `INVALID_REQUEST` (-32600) error with a different message) confirm reconnection is not triggered for either. - Full suite passes (30/30). - Verified end-to-end against a local streamable-http MCP server (PHP, `mcp/sdk`-based, `FileSessionStore`): built mcpo from this branch, established a session, deleted the session's backing file to force the server to 404 on the next call, and confirmed the unpatched code returns a 500 to the mcpo client while this patch transparently negotiates a new session and completes the call. Assisted with Claude Code --- <sub>🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.</sub>
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
open-webui/mcpo#251
No description provided.