{"record":{"id":"c3ead62d5122024b","repo":"Hmbown/CodeWhale","slug":"mcp-session-expired-error","errorCode":null,"errorMessage":"MCP session expired: {error}","messagePattern":"MCP session expired: (.+?)","errorType":"exception","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/tui/src/mcp.rs","lineNumber":2234,"sourceCode":"                    };\n                    return Err(err).with_context(|| {\n                        format!(\n                            \"Invalid MCP JSON-RPC message from server '{}': {}\",\n                            self.name, preview\n                        )\n                    });\n                }\n            };\n\n            // Check if this is a response with the expected id. We emit\n            // string IDs because some MCP gateways reject numeric JSON-RPC\n            // IDs, but accept numeric echoes for compatibility with older\n            // servers and tests.\n            if response_id_matches(value.get(\"id\"), &expected_id) {\n                if let Some(error) = value.get(\"error\")\n                    && is_mcp_stale_session_body(&error.to_string())\n                {\n                    anyhow::bail!(\"MCP session expired: {error}\");\n                }\n                return Ok(value);\n            }\n            // Skip notifications (no id) and responses with different ids\n        }\n    }\n\n    /// Gracefully close the connection\n    #[allow(dead_code)] // Public API for MCP consumers\n    pub fn close(&mut self) {\n        self.cancel_token.cancel();\n        self.state = ConnectionState::Disconnected;\n    }\n\n    fn catalog_authorized(&self) -> bool {\n        self.config\n            .reviewed_plugin\n            .as_ref()","sourceCodeStart":2216,"sourceCodeEnd":2252,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/8880682c63083a91624de936797efa3ce9e498fd/crates/tui/src/mcp.rs#L2216-L2252","documentation":"After matching the expected JSON-RPC id, recv inspects the response's error member with is_mcp_stale_session_body; if the server reports an expired/unknown session (typical of MCP streamable-HTTP servers whose session id has aged out or been forgotten), this error is raised instead of returning the raw error. It signals that the MCP session established at initialize time is no longer valid on the server side, while the transport itself may still be alive.","triggerScenarios":"A streamable-HTTP MCP server issues a session id at initialize, then expires or restarts its session store; the next request on the old session returns a JSON-RPC error whose body matches the stale-session detector, and recv converts it to this message.","commonSituations":"Long-lived TUI sessions against an MCP gateway behind a load balancer that rotates sessions; server redeploy mid-session; gateway idle-timeout shorter than the client's connection lifetime; resuming a laptop from sleep with the HTTP session long expired server-side.","solutions":["Reconnect: drop the connection and call get_or_connect again — a fresh initialize handshake obtains a new session id.","If it recurs frequently, check the server/gateway's session TTL and align it with expected idle periods, or enable client-side periodic activity.","Verify nothing between client and server (proxy, LB) strips the session header on responses.","If the server is supposed to be stateless, check its MCP implementation version — a bug may invalidate sessions prematurely."],"exampleFix":"// before\nlet result = pool.get_or_connect(\"remote\").await?.call_tool(...).await;\n// Err: MCP session expired: {...}\n\n// after — drop and re-establish the session\nif let Err(e) = pool.get_or_connect(\"remote\").await?.call_tool(...).await {\n    if e.to_string().contains(\"MCP session expired\") {\n        pool.drop_connection(\"remote\", \"stale session\");\n        return pool.get_or_connect(\"remote\").await?.call_tool(...).await;\n    }\n    return Err(e);\n}","handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"// Rust: stale MCP session -> drop connection, re-initialize, retry once\nlet r = pool.get_or_connect(server).await?.call_tool(n, a, t).await;\nmatch r {\n    Err(e) if e.to_string().contains(\"MCP session expired\") => {\n        pool.drop_connection(server, \"stale session\");\n        pool.get_or_connect(server).await?.call_tool(n, a, t).await\n    }\n    o => o,\n}","preventionTips":["Treat session expiry as expected on long-lived HTTP MCP connections; implement one automatic re-init.","Align gateway/session TTLs with your session lifetime to avoid constant re-auth.","Don't reuse a connection handle after expiry — the session id is baked into it."],"tags":["mcp","session","http","expiry"],"backgroundTag":null,"analyzedSha":"8880682c63083a91624de936797efa3ce9e498fd","analyzedAt":"2026-08-16T11:31:27.956Z","schemaVersion":2},"datasetVersion":"2026-08-16T13:17:31.715Z"}