diff --git a/agent/x/agentmcp/manager.go b/agent/x/agentmcp/manager.go index aa4fcbf0b5..bf47a677ab 100644 --- a/agent/x/agentmcp/manager.go +++ b/agent/x/agentmcp/manager.go @@ -915,6 +915,12 @@ func (m *Manager) createTransport(ctx context.Context, cfg ServerConfig) (mcp.Tr return &mcp.StreamableClientTransport{ Endpoint: cfg.URL, HTTPClient: httpClientWithHeaders(cfg.Headers), + // The SDK opens the standalone GET inside Connect on a context + // connectTimeout does not cancel, so a server that never answers + // it hangs Connect, and every later reload, indefinitely. This + // also means server-initiated handlers on the client never fire + // over HTTP. + DisableStandaloneSSE: true, }, nil case "sse": return &mcp.SSEClientTransport{ diff --git a/agent/x/agentmcp/manager_internal_test.go b/agent/x/agentmcp/manager_internal_test.go index b6023ca59e..8186df2e5c 100644 --- a/agent/x/agentmcp/manager_internal_test.go +++ b/agent/x/agentmcp/manager_internal_test.go @@ -6,6 +6,8 @@ import ( "encoding/base64" "encoding/json" "fmt" + "net/http" + "net/http/httptest" "os" "path/filepath" "testing" @@ -272,6 +274,65 @@ func TestConnectServer_StdioProcessSurvivesConnect(t *testing.T) { assert.Equal(t, "echo", result.Tools[0].Name) } +// TestConnectServer_HTTPConnectsWhenStandaloneSSEHangs guards against the +// SDK's standalone GET, which connectTimeout does not bound, hanging +// Connect. See DisableStandaloneSSE in createTransport. The GET is only +// sent when the negotiated protocol is below 2026-07-28, so this test +// stops exercising it if an SDK upgrade negotiates that version or later. +func TestConnectServer_HTTPConnectsWhenStandaloneSSEHangs(t *testing.T) { + t.Parallel() + + server := mcp.NewServer(&mcp.Implementation{Name: "fake", Version: "1.0.0"}, nil) + server.AddTool(&mcp.Tool{ + Name: "echo", + InputSchema: map[string]any{"type": "object"}, + }, func(context.Context, *mcp.CallToolRequest) (*mcp.CallToolResult, error) { + return &mcp.CallToolResult{}, nil + }) + handler := mcp.NewStreamableHTTPHandler(func(*http.Request) *mcp.Server { return server }, nil) + + release := make(chan struct{}) + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodGet { + select { + case <-release: + case <-r.Context().Done(): + } + return + } + handler.ServeHTTP(w, r) + })) + t.Cleanup(srv.Close) + t.Cleanup(func() { close(release) }) + + ctx := testutil.Context(t, testutil.WaitShort) + m := NewManager(ctx, slogtest.Make(t, nil), agentexec.DefaultExecer, nil, nil, nil, nil) + t.Cleanup(func() { _ = m.Close() }) + + type connectResult struct { + client *mcp.ClientSession + err error + } + done := make(chan connectResult, 1) + go func() { + client, err := m.connectServer(ctx, ServerConfig{ + Name: "fake", + Transport: "http", + URL: srv.URL, + }) + done <- connectResult{client: client, err: err} + }() + + res := testutil.RequireReceive(ctx, t, done) + require.NoError(t, res.err) + t.Cleanup(func() { _ = res.client.Close() }) + + result, err := res.client.ListTools(ctx, nil) + require.NoError(t, err) + require.Len(t, result.Tools, 1) + assert.Equal(t, "echo", result.Tools[0].Name) +} + func TestManager_WaitReloadTimeout(t *testing.T) { t.Parallel()