-
Notifications
You must be signed in to change notification settings - Fork 411
Description
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
-
Install ADK:
go get google.golang.org/[email protected] -
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- 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},
})- Send a request to the agent → works
- Wait a moment (or let the session timeout)
- 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.