@@ -62,6 +62,24 @@ func isHTTPConnectionError(err error) bool {
6262 return false
6363}
6464
65+ // isSessionNotFoundError checks if an error message indicates a backend MCP session has expired
66+ // or is not found. This is used to detect when automatic reconnection to the backend is needed.
67+ func isSessionNotFoundError (err error ) bool {
68+ if err == nil {
69+ return false
70+ }
71+ return strings .Contains (strings .ToLower (err .Error ()), "session not found" )
72+ }
73+
74+ // isSessionNotFoundHTTPResponse checks if an HTTP response indicates the backend session was not found.
75+ // MCP backends return HTTP 404 with a "session not found" body when a session has expired.
76+ func isSessionNotFoundHTTPResponse (statusCode int , body []byte ) bool {
77+ if statusCode != http .StatusNotFound {
78+ return false
79+ }
80+ return strings .Contains (strings .ToLower (string (body )), "session not found" )
81+ }
82+
6583// parseSSEResponse extracts JSON data from SSE-formatted response
6684// SSE format: "event: message\ndata: {json}\n\n"
6785func parseSSEResponse (body []byte ) ([]byte , error ) {
@@ -436,58 +454,45 @@ func (c *Connection) initializeHTTPSession() (string, error) {
436454 return sessionID , nil
437455}
438456
439- // sendHTTPRequest sends a JSON-RPC request to an HTTP MCP server
440- // The ctx parameter is used to extract session ID for the Mcp-Session-Id header
441- func (c * Connection ) sendHTTPRequest (ctx context.Context , method string , params interface {}) (* Response , error ) {
442- // Generate unique request ID using atomic counter
443- requestID := atomic .AddUint64 (& requestIDCounter , 1 )
444-
445- // For tools/call, ensure arguments field always exists (MCP protocol requirement)
446- if method == "tools/call" {
447- params = ensureToolCallArguments (params )
448- }
449-
450- logConn .Printf ("Sending HTTP request to %s: method=%s, id=%d" , c .httpURL , method , requestID )
451-
452- // Execute HTTP request with custom header modification for session ID
453- result , err := c .executeHTTPRequest (ctx , method , params , requestID , func (httpReq * http.Request ) {
454- // Add Mcp-Session-Id header with priority:
455- // 1) Context session ID (if explicitly provided for this request)
456- // 2) Stored httpSessionID from initialization
457+ // buildSessionHeaderModifier returns a header modifier function that adds the Mcp-Session-Id header.
458+ // Priority: context session ID > stored connection session ID.
459+ // The returned function reads c.httpSessionID at call time, so it picks up any reconnected session.
460+ func (c * Connection ) buildSessionHeaderModifier (ctx context.Context ) func (* http.Request ) {
461+ // Capture any context-provided session ID once (it never changes for this request).
462+ ctxSessionID , _ := ctx .Value (SessionIDContextKey ).(string )
463+ return func (httpReq * http.Request ) {
457464 var sessionID string
458- if ctxSessionID , ok := ctx . Value ( SessionIDContextKey ).( string ); ok && ctxSessionID != "" {
465+ if ctxSessionID != "" {
459466 sessionID = ctxSessionID
460467 logConn .Printf ("Using session ID from context: %s" , sessionID )
461468 } else if c .httpSessionID != "" {
462469 sessionID = c .httpSessionID
463470 logConn .Printf ("Using stored session ID from initialization: %s" , sessionID )
464471 }
465-
466472 if sessionID != "" {
467473 httpReq .Header .Set ("Mcp-Session-Id" , sessionID )
468474 } else {
469475 logConn .Printf ("No session ID available (backend may not require session management)" )
470476 }
471- })
472- if err != nil {
473- return nil , err
474477 }
478+ }
475479
476- logConn .Printf ("Received HTTP response: status=%d, body_len=%d" , result .StatusCode , len (result .ResponseBody ))
477-
478- // Parse JSON-RPC response
479- // The response might be in SSE format (event: message\ndata: {...})
480+ // parseHTTPResult converts a raw httpRequestResult into a JSON-RPC Response, handling non-OK
481+ // HTTP status codes by synthesising a JSON-RPC error when the server did not provide one.
482+ func parseHTTPResult (result * httpRequestResult ) (* Response , error ) {
483+ // Parse JSON-RPC response.
484+ // The response might be in SSE format (event: message\ndata: {...}).
480485 rpcResponse , err := parseJSONRPCResponseWithSSE (result .ResponseBody , result .StatusCode , "JSON-RPC response" )
481486 if err != nil {
482487 return nil , err
483488 }
484489
485- // Check for HTTP errors after parsing
490+ // Check for HTTP errors after parsing.
486491 // If we have a non-OK status but successfully parsed a JSON-RPC response,
487- // pass it through (it may already contain an error field)
492+ // pass it through (it may already contain an error field).
488493 if result .StatusCode != http .StatusOK {
489494 logConn .Printf ("HTTP error status=%d with valid JSON-RPC response, passing through" , result .StatusCode )
490- // If the response doesn't already have an error, construct one
495+ // If the response doesn't already have an error, construct one.
491496 if rpcResponse .Error == nil {
492497 rpcResponse .Error = & ResponseError {
493498 Code : - 32603 , // Internal error
@@ -499,3 +504,44 @@ func (c *Connection) sendHTTPRequest(ctx context.Context, method string, params
499504
500505 return rpcResponse , nil
501506}
507+
508+ // sendHTTPRequest sends a JSON-RPC request to an HTTP MCP server.
509+ // The ctx parameter is used to extract session ID for the Mcp-Session-Id header.
510+ // If the backend returns a "session not found" (HTTP 404) response, it attempts a one-time
511+ // session reconnect and retries the request transparently.
512+ func (c * Connection ) sendHTTPRequest (ctx context.Context , method string , params interface {}) (* Response , error ) {
513+ // For tools/call, ensure arguments field always exists (MCP protocol requirement)
514+ if method == "tools/call" {
515+ params = ensureToolCallArguments (params )
516+ }
517+
518+ headerModifier := c .buildSessionHeaderModifier (ctx )
519+
520+ requestID := atomic .AddUint64 (& requestIDCounter , 1 )
521+ logConn .Printf ("Sending HTTP request to %s: method=%s, id=%d" , c .httpURL , method , requestID )
522+
523+ result , err := c .executeHTTPRequest (ctx , method , params , requestID , headerModifier )
524+ if err != nil {
525+ return nil , err
526+ }
527+
528+ logConn .Printf ("Received HTTP response: status=%d, body_len=%d" , result .StatusCode , len (result .ResponseBody ))
529+
530+ // If the backend reported that the session has expired, reconnect and retry once.
531+ if isSessionNotFoundHTTPResponse (result .StatusCode , result .ResponseBody ) {
532+ logConn .Printf ("Session not found from %s (serverID=%s), attempting reconnect" , c .httpURL , c .serverID )
533+ if reconnErr := c .reconnectPlainJSON (); reconnErr == nil {
534+ requestID = atomic .AddUint64 (& requestIDCounter , 1 )
535+ logConn .Printf ("Retrying HTTP request after reconnect: method=%s, id=%d" , method , requestID )
536+ result , err = c .executeHTTPRequest (ctx , method , params , requestID , headerModifier )
537+ if err != nil {
538+ return nil , err
539+ }
540+ logConn .Printf ("Retry HTTP response: status=%d, body_len=%d" , result .StatusCode , len (result .ResponseBody ))
541+ } else {
542+ logConn .Printf ("Session reconnect failed (%v), returning original session-not-found error" , reconnErr )
543+ }
544+ }
545+
546+ return parseHTTPResult (result )
547+ }
0 commit comments