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.
This commit is contained in:
dtapps 2026-04-30 22:56:40 +08:00
parent dbf5d9ce1f
commit 8d4cc3fbba
4 changed files with 20 additions and 10 deletions

View file

@ -132,7 +132,8 @@ func (al *AgentLoop) Run(ctx context.Context) error {
return err return err
} }
if err := al.ensureMCPInitialized(ctx); err != nil { 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) idleTicker := time.NewTicker(100 * time.Millisecond)

View file

@ -48,7 +48,8 @@ func (al *AgentLoop) ProcessDirectWithChannel(
return "", err return "", err
} }
if err := al.ensureMCPInitialized(ctx); err != nil { 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{ msg := bus.InboundMessage{
@ -73,7 +74,8 @@ func (al *AgentLoop) ProcessHeartbeat(
return "", err return "", err
} }
if err := al.ensureMCPInitialized(ctx); err != nil { 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() agent := al.GetRegistry().GetDefaultAgent()

View file

@ -370,8 +370,8 @@ func (al *AgentLoop) Continue(ctx context.Context, sessionKey, channel, chatID s
return "", err return "", err
} }
if err := al.ensureMCPInitialized(ctx); err != nil { if err := al.ensureMCPInitialized(ctx); err != nil {
al.activeTurnStates.Delete(sessionKey) logger.WarnCF("agent", "MCP initialization failed, continuing steering without MCP tools",
return "", err map[string]any{"error": err.Error(), "session_key": sessionKey})
} }
steeringMsgs := al.dequeueSteeringMessagesForScopeWithFallback(sessionKey) steeringMsgs := al.dequeueSteeringMessagesForScopeWithFallback(sessionKey)

View file

@ -12,6 +12,7 @@ import (
"strings" "strings"
"sync" "sync"
"sync/atomic" "sync/atomic"
"time"
"github.com/modelcontextprotocol/go-sdk/mcp" "github.com/modelcontextprotocol/go-sdk/mcp"
@ -341,14 +342,18 @@ func connectServer(
DisableStandaloneSSE: disableStandaloneSSE, 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 // Add custom headers if provided
if len(cfg.Headers) > 0 { if len(cfg.Headers) > 0 {
// Create a custom HTTP client with header-injecting transport // Create a custom HTTP client with header-injecting transport
sseTransport.HTTPClient = &http.Client{ mcpHTTPClient.Transport = &headerTransport{
Transport: &headerTransport{
base: http.DefaultTransport, base: http.DefaultTransport,
headers: cfg.Headers, headers: cfg.Headers,
},
} }
logger.DebugCF("mcp", "Added custom HTTP headers", logger.DebugCF("mcp", "Added custom HTTP headers",
map[string]any{ map[string]any{
@ -357,6 +362,8 @@ func connectServer(
}) })
} }
sseTransport.HTTPClient = mcpHTTPClient
transport = sseTransport transport = sseTransport
case "stdio": case "stdio":
if cfg.Command == "" { if cfg.Command == "" {