diff --git a/internal/handler/config.go b/internal/handler/config.go index 07f0a02a..94e01d1d 100644 --- a/internal/handler/config.go +++ b/internal/handler/config.go @@ -465,24 +465,26 @@ func (h *ConfigHandler) UpdateConfig(c *gin.Context) { // 在循环外部统一更新,避免重复调用 h.externalMCPMgr.LoadConfigs(&h.config.ExternalMCP) - // 处理MCP连接状态 + // 处理MCP连接状态(异步启动,避免阻塞) for mcpName := range externalMCPToolMap { cfg := h.config.ExternalMCP.Servers[mcpName] // 如果MCP需要启用,确保客户端已启动 if cfg.ExternalMCPEnable { - // 启动外部MCP(如果未启动) + // 启动外部MCP(如果未启动)- 异步执行,避免阻塞 client, exists := h.externalMCPMgr.GetClient(mcpName) if !exists || !client.IsConnected() { - if err := h.externalMCPMgr.StartClient(mcpName); err != nil { - h.logger.Warn("启动外部MCP失败", - zap.String("mcp", mcpName), - zap.Error(err), - ) - } else { - h.logger.Info("启动外部MCP", - zap.String("mcp", mcpName), - ) - } + go func(name string) { + if err := h.externalMCPMgr.StartClient(name); err != nil { + h.logger.Warn("启动外部MCP失败", + zap.String("mcp", name), + zap.Error(err), + ) + } else { + h.logger.Info("启动外部MCP", + zap.String("mcp", name), + ) + } + }(mcpName) } } } diff --git a/internal/handler/external_mcp.go b/internal/handler/external_mcp.go index 4e0add98..bd68a6af 100644 --- a/internal/handler/external_mcp.go +++ b/internal/handler/external_mcp.go @@ -56,11 +56,16 @@ func (h *ExternalMCPHandler) GetExternalMCPs(c *gin.Context) { } toolCount := toolCounts[name] + errorMsg := "" + if status == "error" { + errorMsg = h.manager.GetError(name) + } result[name] = ExternalMCPResponse{ Config: cfg, Status: status, ToolCount: toolCount, + Error: errorMsg, } } @@ -102,10 +107,17 @@ func (h *ExternalMCPHandler) GetExternalMCP(c *gin.Context) { } } + // 获取错误信息 + errorMsg := "" + if status == "error" { + errorMsg = h.manager.GetError(name) + } + c.JSON(http.StatusOK, ExternalMCPResponse{ Config: cfg, Status: status, ToolCount: toolCount, + Error: errorMsg, }) } @@ -238,7 +250,7 @@ func (h *ExternalMCPHandler) StartExternalMCP(c *gin.Context) { return } - // 启动客户端(这可能会花费一些时间) + // 启动客户端(立即创建客户端并设置状态为connecting,实际连接在后台进行) h.logger.Info("开始启动外部MCP", zap.String("name", name)) if err := h.manager.StartClient(name); err != nil { h.logger.Error("启动外部MCP失败", zap.String("name", name), zap.Error(err)) @@ -249,16 +261,17 @@ func (h *ExternalMCPHandler) StartExternalMCP(c *gin.Context) { return } - // 获取连接状态 + // 获取客户端状态(应该是connecting) client, exists := h.manager.GetClient(name) - status := "disconnected" + status := "connecting" if exists { status = client.GetStatus() } - h.logger.Info("外部MCP启动完成", zap.String("name", name), zap.String("status", status)) + // 立即返回,不等待连接完成 + // 客户端会在后台异步连接,用户可以通过状态查询接口查看连接状态 c.JSON(http.StatusOK, gin.H{ - "message": "外部MCP启动完成", + "message": "外部MCP启动请求已提交,正在后台连接中", "status": status, }) } @@ -504,7 +517,8 @@ type AddOrUpdateExternalMCPRequest struct { // ExternalMCPResponse 外部MCP响应 type ExternalMCPResponse struct { Config config.ExternalMCPServerConfig `json:"config"` - Status string `json:"status"` // "connected", "disconnected", "disabled", "error" + Status string `json:"status"` // "connected", "disconnected", "disabled", "error", "connecting" ToolCount int `json:"tool_count"` // 工具数量 + Error string `json:"error,omitempty"` // 错误信息(仅在status为error时存在) } diff --git a/internal/mcp/external_manager.go b/internal/mcp/external_manager.go index a77a99bc..fb4d2530 100644 --- a/internal/mcp/external_manager.go +++ b/internal/mcp/external_manager.go @@ -22,6 +22,7 @@ type ExternalMCPManager struct { storage MonitorStorage // 可选的持久化存储 executions map[string]*ToolExecution // 执行记录 stats map[string]*ToolStats // 工具统计信息 + errors map[string]string // 错误信息 mu sync.RWMutex } @@ -39,6 +40,7 @@ func NewExternalMCPManagerWithStorage(logger *zap.Logger, storage MonitorStorage storage: storage, executions: make(map[string]*ToolExecution), stats: make(map[string]*ToolStats), + errors: make(map[string]string), } } @@ -117,12 +119,12 @@ func (m *ExternalMCPManager) StartClient(name string) error { // 检查是否已经有连接的客户端 m.mu.RLock() - _, hasClient := m.clients[name] + existingClient, hasClient := m.clients[name] m.mu.RUnlock() if hasClient { // 检查客户端是否已连接 - if client, ok := m.GetClient(name); ok && client.IsConnected() { + if existingClient.IsConnected() { // 客户端已连接,直接返回成功(目标状态已达成) // 更新配置为启用(确保配置一致) m.mu.Lock() @@ -132,22 +134,55 @@ func (m *ExternalMCPManager) StartClient(name string) error { return nil } // 如果有客户端但未连接,先关闭 - if client, ok := m.GetClient(name); ok { - client.Close() - m.mu.Lock() - delete(m.clients, name) - m.mu.Unlock() - } + existingClient.Close() + m.mu.Lock() + delete(m.clients, name) + m.mu.Unlock() } // 更新配置为启用 m.mu.Lock() serverCfg.ExternalMCPEnable = true m.configs[name] = serverCfg + // 清除之前的错误信息(重新启动时) + delete(m.errors, name) m.mu.Unlock() - // 连接客户端 - return m.connectClient(name, serverCfg) + // 立即创建客户端并设置为"connecting"状态,这样前端可以立即看到状态 + client := m.createClient(serverCfg) + if client == nil { + return fmt.Errorf("无法创建客户端:不支持的传输模式") + } + + // 设置状态为connecting + m.setClientStatus(client, "connecting") + + // 立即保存客户端,这样前端查询时就能看到"connecting"状态 + m.mu.Lock() + m.clients[name] = client + m.mu.Unlock() + + // 在后台异步进行实际连接 + go func() { + if err := m.doConnect(name, serverCfg, client); err != nil { + m.logger.Error("连接外部MCP客户端失败", + zap.String("name", name), + zap.Error(err), + ) + // 连接失败,设置状态为error并保存错误信息 + m.setClientStatus(client, "error") + m.mu.Lock() + m.errors[name] = err.Error() + m.mu.Unlock() + } else { + // 连接成功,清除错误信息 + m.mu.Lock() + delete(m.errors, name) + m.mu.Unlock() + } + }() + + return nil } // StopClient 停止客户端 @@ -166,6 +201,9 @@ func (m *ExternalMCPManager) StopClient(name string) error { delete(m.clients, name) } + // 清除错误信息 + delete(m.errors, name) + // 更新配置为禁用 serverCfg.ExternalMCPEnable = false m.configs[name] = serverCfg @@ -182,6 +220,14 @@ func (m *ExternalMCPManager) GetClient(name string) (ExternalMCPClient, bool) { return client, exists } +// GetError 获取错误信息 +func (m *ExternalMCPManager) GetError(name string) string { + m.mu.RLock() + defer m.mu.RUnlock() + + return m.errors[name] +} + // GetAllTools 获取所有外部MCP的工具 func (m *ExternalMCPManager) GetAllTools(ctx context.Context) ([]Tool, error) { m.mu.RLock() @@ -543,10 +589,8 @@ func (m *ExternalMCPManager) GetToolCounts() map[string]int { return result } -// connectClient 连接客户端(异步) -func (m *ExternalMCPManager) connectClient(name string, serverCfg config.ExternalMCPServerConfig) error { - var client ExternalMCPClient - +// createClient 创建客户端(不连接) +func (m *ExternalMCPManager) createClient(serverCfg config.ExternalMCPServerConfig) ExternalMCPClient { timeout := time.Duration(serverCfg.Timeout) * time.Second if timeout <= 0 { timeout = 30 * time.Second @@ -561,29 +605,77 @@ func (m *ExternalMCPManager) connectClient(name string, serverCfg config.Externa } else if serverCfg.URL != "" { transport = "http" } else { - return fmt.Errorf("无法确定传输模式: 需要指定command或url") + return nil } } switch transport { case "http": if serverCfg.URL == "" { - return fmt.Errorf("HTTP模式需要URL") + return nil } - client = NewHTTPMCPClient(serverCfg.URL, timeout, m.logger) + return NewHTTPMCPClient(serverCfg.URL, timeout, m.logger) case "stdio": if serverCfg.Command == "" { - return fmt.Errorf("stdio模式需要command") + return nil } - client = NewStdioMCPClient(serverCfg.Command, serverCfg.Args, timeout, m.logger) + return NewStdioMCPClient(serverCfg.Command, serverCfg.Args, timeout, m.logger) default: - return fmt.Errorf("不支持的传输模式: %s", transport) + return nil + } +} + +// doConnect 执行实际连接 +func (m *ExternalMCPManager) doConnect(name string, serverCfg config.ExternalMCPServerConfig, client ExternalMCPClient) error { + timeout := time.Duration(serverCfg.Timeout) * time.Second + if timeout <= 0 { + timeout = 30 * time.Second } // 初始化连接 ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() + if err := client.Initialize(ctx); err != nil { + return err + } + + m.logger.Info("外部MCP客户端已连接", + zap.String("name", name), + ) + + return nil +} + +// setClientStatus 设置客户端状态(通过类型断言) +func (m *ExternalMCPManager) setClientStatus(client ExternalMCPClient, status string) { + switch c := client.(type) { + case *HTTPMCPClient: + c.setStatus(status) + case *StdioMCPClient: + c.setStatus(status) + } +} + +// connectClient 连接客户端(异步)- 保留用于向后兼容 +func (m *ExternalMCPManager) connectClient(name string, serverCfg config.ExternalMCPServerConfig) error { + client := m.createClient(serverCfg) + if client == nil { + return fmt.Errorf("无法创建客户端:不支持的传输模式") + } + + // 设置状态为connecting + m.setClientStatus(client, "connecting") + + // 初始化连接 + timeout := time.Duration(serverCfg.Timeout) * time.Second + if timeout <= 0 { + timeout = 30 * time.Second + } + + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + if err := client.Initialize(ctx); err != nil { m.logger.Error("初始化外部MCP客户端失败", zap.String("name", name), @@ -599,7 +691,6 @@ func (m *ExternalMCPManager) connectClient(name string, serverCfg config.Externa m.logger.Info("外部MCP客户端已连接", zap.String("name", name), - zap.String("transport", transport), ) return nil diff --git a/web/static/css/style.css b/web/static/css/style.css index a727fc51..4335be03 100644 --- a/web/static/css/style.css +++ b/web/static/css/style.css @@ -2331,6 +2331,18 @@ header { border: 1px solid rgba(255, 193, 7, 0.3); } +.external-mcp-status.status-error { + background: rgba(220, 53, 69, 0.12); + color: var(--error-color); + border: 1px solid rgba(220, 53, 69, 0.3); +} + +.external-mcp-status.status-error::before { + content: '❌'; + display: inline-block; + margin-right: 4px; +} + .external-mcp-item-actions { display: flex; align-items: center; diff --git a/web/static/js/app.js b/web/static/js/app.js index de332672..542a20d7 100644 --- a/web/static/js/app.js +++ b/web/static/js/app.js @@ -2652,9 +2652,11 @@ function renderExternalMCPList(servers) { const status = server.status || 'disconnected'; const statusClass = status === 'connected' ? 'status-connected' : status === 'connecting' ? 'status-connecting' : + status === 'error' ? 'status-error' : status === 'disabled' ? 'status-disabled' : 'status-disconnected'; const statusText = status === 'connected' ? '已连接' : status === 'connecting' ? '连接中...' : + status === 'error' ? '连接失败' : status === 'disabled' ? '已禁用' : '未连接'; const transport = server.config.transport || (server.config.command ? 'stdio' : 'http'); const transportIcon = transport === 'stdio' ? '⚙️' : '🌐'; @@ -2667,7 +2669,7 @@ function renderExternalMCPList(servers) { ${statusText}