Skip to content

Proposal: Provide better error handling for HTTP errors when using a Streamable ClientΒ #579

@ravanscafi

Description

@ravanscafi

Is your feature request related to a problem? Please describe.

When an MCP server responds to an initialize request with an error status (e.g., 401 Unauthorized), the current error handling in client.Connect() only provides the HTTP status code as part of the error message itself.
Important context like response headers (e.g., WWW-Authenticate), response body, and other HTTP metadata are not accessible to the caller.

This makes it difficult to implement proper authentication flows or provide detailed error messages to users, as critical information needed to handle the error appropriately is lost.

Describe the solution you'd like

Expose HTTP response details in connection errors, potentially through:

  1. A structured error type that includes:

    • HTTP status code
    • Response headers
    • Response body
    • Original error
  2. Example API:

type HTTPError struct {
    StatusCode int
    Headers    http.Header
    Body       []byte
    Err        error
}

func (e *HTTPError) Error() string { ... }
func (e *HTTPError) Unwrap() error { return e.Err }

This would allow clients to handle authentication challenges and other HTTP-level errors appropriately:

session, err := client.Connect(ctx, transport, nil)
if err != nil {
    var httpErr *mcp.HTTPError
    if errors.As(err, &httpErr) {
        if httpErr.StatusCode == 401 {
        // Access WWW-Authenticate header
        authHeader := httpErr.Headers.Get("WWW-Authenticate")
        // Handle authentication flow
        }
    }
}

Describe alternatives you've considered

Wrapping the HTTP client in Transport options:
While this works, it adds significant complexity:

  • Requires manual synchronization (mutexes) to safely capture response data when the session is used concurrently
  • Couples transport-level concerns with business logic
  • Makes the code harder to maintain and test

Additional context

Minimal reproduction example:

package main

import (
	"context"
	"log"
	"net/http"

	"github.com/modelcontextprotocol/go-sdk/mcp"
)

func main() {
	go runServer("localhost:8000")
	runClient("http://localhost:8000")
}

func runServer(url string) {
	handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`)
		w.WriteHeader(http.StatusUnauthorized)
	})

	log.Printf("[Server] MCP server listening on %s", url)

	if err := http.ListenAndServe(url, handler); err != nil {
		log.Fatalf("[Server] Server failed: %v", err)
	}
}

func runClient(url string) {
	ctx := context.Background()

	log.Printf("[Client] Connecting to MCP server at %s", url)

	client := mcp.NewClient(&mcp.Implementation{}, nil)

	session, err := client.Connect(ctx, &mcp.StreamableClientTransport{Endpoint: url}, nil)
	if err != nil {
		// Currently: no access to WWW-Authenticate header or response body
		log.Fatalf("[Client] Failed to connect: %v (type: %T)", err, err)
	}
	defer session.Close()

	log.Println("[Client] Session acquired successfully")
}

Output:

2025/10/14 16:41:25 [Client] Connecting to MCP server at http://localhost:8000
2025/10/14 16:41:25 [Server] MCP server listening on localhost:8000
2025/10/14 16:41:25 [Client] Failed to connect: calling "initialize": broken session: 401 Unauthorized (type: *fmt.wrapError)
exit status 1

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions