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 == "" {