Skip to content

mcptoolset: cached MCP session not validated, causes "connection closed" errors on subsequent requests #399

@mikaelhatanpaa

Description

@mikaelhatanpaa

Describe the bug

The mcptoolset package caches the *mcp.ClientSession after the first connection and never validates if it's still alive before returning it. For HTTP/SSE-based MCP servers (using StreamableClientTransport), sessions can become stale due to server-side session timeouts, network interruptions, or idle connection cleanup.

When the cached session becomes stale, all subsequent requests fail with errors like:

connection closed: calling "tools/list": client is closing: standalone SSE stream: failed to reconnect (session ID: ...): connection failed after 5 attempts

The first request always works, but subsequent requests fail.

To Reproduce

  1. Install ADK: go get google.golang.org/[email protected]

  2. Create an MCP server using the Go MCP SDK with HTTP transport:

import mcpsdk "github.com/modelcontextprotocol/go-sdk/mcp"

srv := mcpsdk.NewServer(&mcpsdk.Implementation{Name: "test-server", Version: "1.0.0"}, nil)
handler := mcpsdk.NewStreamableHTTPHandler(func(r *http.Request) *mcpsdk.Server {
    return srv
}, &mcpsdk.StreamableHTTPOptions{
    SessionTimeout: 5 * time.Minute,
})
// Serve on http://localhost:8080/mcp
  1. Create an ADK agent that connects to this MCP server via HTTP:
transport := &mcp.StreamableClientTransport{
    Endpoint: "http://localhost:8080/mcp",
}
toolset, _ := mcptoolset.New(mcptoolset.Config{
    Transport: transport,
})
agent, _ := llmagent.New(llmagent.Config{
    Name:     "test-agent",
    Model:    model,
    Toolsets: []tool.Toolset{toolset},
})
  1. Send a request to the agent → works
  2. Wait a moment (or let the session timeout)
  3. Send another request → fails with "connection closed" error

Root cause

From tool/mcptoolset/set.go:

func (s *set) getSession(ctx context.Context) (*mcp.ClientSession, error) {
    s.mu.Lock()
    defer s.mu.Unlock()
    if s.session != nil {
        return s.session, nil  // ← Always returns cached session without checking if still valid
    }
    session, err := s.client.Connect(ctx, s.transport, nil)
    // ...
    s.session = session
    return s.session, nil
}

Expected behavior

The mcptoolset should handle stale sessions gracefully by checking if the session is still valid before returning the cached session, and clearing it to trigger reconnection if it's stale.

Suggested fix

func (s *set) getSession(ctx context.Context) (*mcp.ClientSession, error) {
    s.mu.Lock()
    defer s.mu.Unlock()

    if s.session != nil {
        // TODO: Check if session is still valid before returning
        // isSessionValid() is to be implemented - could use Ping, check connection state, etc.
        // if !isSessionValid(s.session) {
        //     s.session.Close()
        //     s.session = nil
        // } else {
        //     return s.session, nil
        // }
        return s.session, nil
    }

    session, err := s.client.Connect(ctx, s.transport, nil)
    if err != nil {
        return nil, fmt.Errorf("failed to init MCP session: %w", err)
    }

    s.session = session
    return s.session, nil
}

Screenshots

N/A

Desktop (please complete the following information):

  • OS: macOS (Apple Silicon)
  • Go version: 1.23
  • ADK version: v0.2.0

Model Information:

  • gemini-2.5-flash (but the issue is transport-related, not model-related)

Additional context

  • This issue only affects HTTP-based MCP servers (using StreamableClientTransport).
  • Workaround: For same-process scenarios, using in-memory transport (mcp.NewInMemoryTransports()) avoids the issue entirely.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions