From 8d4cc3fbba13f1825d1903c38c0ee39061852bdf Mon Sep 17 00:00:00 2001 From: dtapps Date: Thu, 30 Apr 2026 22:56:40 +0800 Subject: [PATCH] fix(agent): make MCP initialization failure non-fatal When all MCP servers fail to connect (e.g., network unreachable), the agent loop would exit immediately, leaving the application in a zombie state where the gateway is running but no messages can be processed. Changes: - Downgrade MCP init failure from fatal error to warning in Run(), ProcessDirectWithChannel(), ProcessHeartbeat(), and Continue() - Add 30s timeout to HTTP client used for SSE/HTTP MCP transports to prevent indefinite blocking on unreachable servers The agent now continues operating without MCP tools when servers are unavailable, rather than becoming completely unresponsive. --- pkg/agent/agent.go | 3 ++- pkg/agent/agent_message.go | 6 ++++-- pkg/agent/steering.go | 4 ++-- pkg/mcp/manager.go | 17 ++++++++++++----- 4 files changed, 20 insertions(+), 10 deletions(-) diff --git a/pkg/agent/agent.go b/pkg/agent/agent.go index 2c456dca7..5b2d6878f 100644 --- a/pkg/agent/agent.go +++ b/pkg/agent/agent.go @@ -132,7 +132,8 @@ func (al *AgentLoop) Run(ctx context.Context) error { return err } if err := al.ensureMCPInitialized(ctx); err != nil { - return err + logger.WarnCF("agent", "MCP initialization failed, continuing without MCP tools", + map[string]any{"error": err.Error()}) } idleTicker := time.NewTicker(100 * time.Millisecond) diff --git a/pkg/agent/agent_message.go b/pkg/agent/agent_message.go index 96b0b0817..37dbb4190 100644 --- a/pkg/agent/agent_message.go +++ b/pkg/agent/agent_message.go @@ -48,7 +48,8 @@ func (al *AgentLoop) ProcessDirectWithChannel( return "", err } if err := al.ensureMCPInitialized(ctx); err != nil { - return "", err + logger.WarnCF("agent", "MCP initialization failed, processing without MCP tools", + map[string]any{"error": err.Error()}) } msg := bus.InboundMessage{ @@ -73,7 +74,8 @@ func (al *AgentLoop) ProcessHeartbeat( return "", err } if err := al.ensureMCPInitialized(ctx); err != nil { - return "", err + logger.WarnCF("agent", "MCP initialization failed, processing heartbeat without MCP tools", + map[string]any{"error": err.Error()}) } agent := al.GetRegistry().GetDefaultAgent() diff --git a/pkg/agent/steering.go b/pkg/agent/steering.go index 2efa7bbf4..7e9058049 100644 --- a/pkg/agent/steering.go +++ b/pkg/agent/steering.go @@ -370,8 +370,8 @@ func (al *AgentLoop) Continue(ctx context.Context, sessionKey, channel, chatID s return "", err } if err := al.ensureMCPInitialized(ctx); err != nil { - al.activeTurnStates.Delete(sessionKey) - return "", err + logger.WarnCF("agent", "MCP initialization failed, continuing steering without MCP tools", + map[string]any{"error": err.Error(), "session_key": sessionKey}) } steeringMsgs := al.dequeueSteeringMessagesForScopeWithFallback(sessionKey) diff --git a/pkg/mcp/manager.go b/pkg/mcp/manager.go index 92ea426a6..5de0f866a 100644 --- a/pkg/mcp/manager.go +++ b/pkg/mcp/manager.go @@ -12,6 +12,7 @@ import ( "strings" "sync" "sync/atomic" + "time" "github.com/modelcontextprotocol/go-sdk/mcp" @@ -341,14 +342,18 @@ func connectServer( DisableStandaloneSSE: disableStandaloneSSE, } + // Set up HTTP client with a connection timeout to avoid hanging + // indefinitely when the MCP server is unreachable. + mcpHTTPClient := &http.Client{ + Timeout: 30 * time.Second, + } + // Add custom headers if provided if len(cfg.Headers) > 0 { // Create a custom HTTP client with header-injecting transport - sseTransport.HTTPClient = &http.Client{ - Transport: &headerTransport{ - base: http.DefaultTransport, - headers: cfg.Headers, - }, + mcpHTTPClient.Transport = &headerTransport{ + base: http.DefaultTransport, + headers: cfg.Headers, } logger.DebugCF("mcp", "Added custom HTTP headers", map[string]any{ @@ -357,6 +362,8 @@ func connectServer( }) } + sseTransport.HTTPClient = mcpHTTPClient + transport = sseTransport case "stdio": if cfg.Command == "" {