From 2b6b67843980660584ce3c56ad89171dcadb70dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=85=AC=E6=98=8E?= <83812544+Ed1s0nZ@users.noreply.github.com> Date: Wed, 4 Feb 2026 19:39:29 +0800 Subject: [PATCH] Add files via upload --- internal/handler/config.go | 291 ++++++++++++++++--------------- internal/mcp/external_manager.go | 185 ++++++++++++++++++-- web/static/js/router.js | 13 +- web/static/js/settings.js | 43 ++++- 4 files changed, 367 insertions(+), 165 deletions(-) diff --git a/internal/handler/config.go b/internal/handler/config.go index 6d1714c6..78e65577 100644 --- a/internal/handler/config.go +++ b/internal/handler/config.go @@ -215,61 +215,10 @@ func (h *ConfigHandler) GetConfig(c *gin.Context) { // 获取外部MCP工具 if h.externalMCPMgr != nil { - // 增加超时时间到30秒,因为通过代理连接远程服务器可能需要更长时间 - ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) - defer cancel() - - externalTools, err := h.externalMCPMgr.GetAllTools(ctx) - if err == nil { - externalMCPConfigs := h.externalMCPMgr.GetConfigs() - for _, externalTool := range externalTools { - var mcpName, actualToolName string - if idx := strings.Index(externalTool.Name, "::"); idx > 0 { - mcpName = externalTool.Name[:idx] - actualToolName = externalTool.Name[idx+2:] - } else { - continue - } - - enabled := false - if cfg, exists := externalMCPConfigs[mcpName]; exists { - // 首先检查外部MCP是否启用 - if !cfg.ExternalMCPEnable && !(cfg.Enabled && !cfg.Disabled) { - enabled = false // MCP未启用,所有工具都禁用 - } else { - // MCP已启用,检查单个工具的启用状态 - // 如果ToolEnabled为空或未设置该工具,默认为启用(向后兼容) - if cfg.ToolEnabled == nil { - enabled = true // 未设置工具状态,默认为启用 - } else if toolEnabled, exists := cfg.ToolEnabled[actualToolName]; exists { - enabled = toolEnabled // 使用配置的工具状态 - } else { - enabled = true // 工具未在配置中,默认为启用 - } - } - } - - client, exists := h.externalMCPMgr.GetClient(mcpName) - if !exists || !client.IsConnected() { - enabled = false - } - - description := externalTool.ShortDescription - if description == "" { - description = externalTool.Description - } - if len(description) > 100 { - description = description[:100] + "..." - } - - tools = append(tools, ToolConfigInfo{ - Name: actualToolName, - Description: description, - Enabled: enabled, - IsExternal: true, - ExternalMCP: mcpName, - }) - } + ctx := context.Background() + externalTools := h.getExternalMCPTools(ctx) + for _, toolInfo := range externalTools { + tools = append(tools, toolInfo) } } @@ -451,99 +400,43 @@ func (h *ConfigHandler) GetTools(c *gin.Context) { // 获取外部MCP工具 if h.externalMCPMgr != nil { - // 增加超时时间到30秒,因为通过代理连接远程服务器可能需要更长时间 - ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) - defer cancel() + // 创建context用于获取外部工具 + ctx := context.Background() + externalTools := h.getExternalMCPTools(ctx) - externalTools, err := h.externalMCPMgr.GetAllTools(ctx) - if err != nil { - h.logger.Warn("获取外部MCP工具失败", zap.Error(err)) - } else { - // 获取外部MCP配置,用于判断启用状态 - externalMCPConfigs := h.externalMCPMgr.GetConfigs() - - for _, externalTool := range externalTools { - // 解析工具名称:mcpName::toolName - var mcpName, actualToolName string - if idx := strings.Index(externalTool.Name, "::"); idx > 0 { - mcpName = externalTool.Name[:idx] - actualToolName = externalTool.Name[idx+2:] - } else { - continue // 跳过格式不正确的工具 + // 应用搜索过滤和角色配置 + for _, toolInfo := range externalTools { + // 搜索过滤 + if searchTermLower != "" { + nameLower := strings.ToLower(toolInfo.Name) + descLower := strings.ToLower(toolInfo.Description) + if !strings.Contains(nameLower, searchTermLower) && !strings.Contains(descLower, searchTermLower) { + continue // 不匹配,跳过 } - - // 获取外部工具的启用状态 - enabled := false - if cfg, exists := externalMCPConfigs[mcpName]; exists { - // 首先检查外部MCP是否启用 - if !cfg.ExternalMCPEnable && !(cfg.Enabled && !cfg.Disabled) { - enabled = false // MCP未启用,所有工具都禁用 - } else { - // MCP已启用,检查单个工具的启用状态 - // 如果ToolEnabled为空或未设置该工具,默认为启用(向后兼容) - if cfg.ToolEnabled == nil { - enabled = true // 未设置工具状态,默认为启用 - } else if toolEnabled, exists := cfg.ToolEnabled[actualToolName]; exists { - enabled = toolEnabled // 使用配置的工具状态 - } else { - enabled = true // 工具未在配置中,默认为启用 - } - } - } - - // 检查外部MCP是否已连接 - client, exists := h.externalMCPMgr.GetClient(mcpName) - if !exists || !client.IsConnected() { - enabled = false // 未连接时视为禁用 - } - - description := externalTool.ShortDescription - if description == "" { - description = externalTool.Description - } - if len(description) > 100 { - description = description[:100] + "..." - } - - // 如果有关键词,进行搜索过滤 - if searchTermLower != "" { - nameLower := strings.ToLower(actualToolName) - descLower := strings.ToLower(description) - if !strings.Contains(nameLower, searchTermLower) && !strings.Contains(descLower, searchTermLower) { - continue // 不匹配,跳过 - } - } - - toolInfo := ToolConfigInfo{ - Name: actualToolName, // 显示实际工具名称,不带前缀 - Description: description, - Enabled: enabled, - IsExternal: true, - ExternalMCP: mcpName, - } - - // 根据角色配置标注工具状态 - if roleName != "" { - if roleUsesAllTools { - // 角色使用所有工具,标注启用的工具为role_enabled=true - toolInfo.RoleEnabled = &enabled - } else { - // 角色配置了工具列表,检查工具是否在列表中 - // 外部工具使用 "mcpName::toolName" 格式作为key - externalToolKey := externalTool.Name // 这是 "mcpName::toolName" 格式 - if roleToolsSet[externalToolKey] { - roleEnabled := enabled // 工具必须在角色列表中且本身启用 - toolInfo.RoleEnabled = &roleEnabled - } else { - // 不在角色列表中,标记为false - roleEnabled := false - toolInfo.RoleEnabled = &roleEnabled - } - } - } - - allTools = append(allTools, toolInfo) } + + // 根据角色配置标注工具状态 + if roleName != "" { + if roleUsesAllTools { + // 角色使用所有工具,标注启用的工具为role_enabled=true + roleEnabled := toolInfo.Enabled + toolInfo.RoleEnabled = &roleEnabled + } else { + // 角色配置了工具列表,检查工具是否在列表中 + // 外部工具使用 "mcpName::toolName" 格式作为key + externalToolKey := fmt.Sprintf("%s::%s", toolInfo.ExternalMCP, toolInfo.Name) + if roleToolsSet[externalToolKey] { + roleEnabled := toolInfo.Enabled // 工具必须在角色列表中且本身启用 + toolInfo.RoleEnabled = &roleEnabled + } else { + // 不在角色列表中,标记为false + roleEnabled := false + toolInfo.RoleEnabled = &roleEnabled + } + } + } + + allTools = append(allTools, toolInfo) } } @@ -1259,3 +1152,111 @@ func setFloatInMap(mapNode *yaml.Node, key string, value float64) { valueNode.Value = fmt.Sprintf("%g", value) } } + +// getExternalMCPTools 获取外部MCP工具列表(公共方法) +// 返回 ToolConfigInfo 列表,已处理启用状态和描述信息 +func (h *ConfigHandler) getExternalMCPTools(ctx context.Context) []ToolConfigInfo { + var result []ToolConfigInfo + + if h.externalMCPMgr == nil { + return result + } + + // 使用较短的超时时间(5秒)进行快速失败,避免阻塞页面加载 + timeoutCtx, cancel := context.WithTimeout(ctx, 5*time.Second) + defer cancel() + + externalTools, err := h.externalMCPMgr.GetAllTools(timeoutCtx) + if err != nil { + // 记录警告但不阻塞,继续返回已缓存的工具(如果有) + h.logger.Warn("获取外部MCP工具失败(可能连接断开),尝试返回缓存的工具", + zap.Error(err), + zap.String("hint", "如果外部MCP工具未显示,请检查连接状态或点击刷新按钮"), + ) + } + + // 如果获取到了工具(即使有错误),继续处理 + if len(externalTools) == 0 { + return result + } + + externalMCPConfigs := h.externalMCPMgr.GetConfigs() + + for _, externalTool := range externalTools { + // 解析工具名称:mcpName::toolName + mcpName, actualToolName := h.parseExternalToolName(externalTool.Name) + if mcpName == "" || actualToolName == "" { + continue // 跳过格式不正确的工具 + } + + // 计算启用状态 + enabled := h.calculateExternalToolEnabled(mcpName, actualToolName, externalMCPConfigs) + + // 处理描述信息 + description := h.formatToolDescription(externalTool.ShortDescription, externalTool.Description) + + result = append(result, ToolConfigInfo{ + Name: actualToolName, + Description: description, + Enabled: enabled, + IsExternal: true, + ExternalMCP: mcpName, + }) + } + + return result +} + +// parseExternalToolName 解析外部工具名称(格式:mcpName::toolName) +func (h *ConfigHandler) parseExternalToolName(fullName string) (mcpName, toolName string) { + idx := strings.Index(fullName, "::") + if idx > 0 { + return fullName[:idx], fullName[idx+2:] + } + return "", "" +} + +// calculateExternalToolEnabled 计算外部工具的启用状态 +func (h *ConfigHandler) calculateExternalToolEnabled(mcpName, toolName string, configs map[string]config.ExternalMCPServerConfig) bool { + cfg, exists := configs[mcpName] + if !exists { + return false + } + + // 首先检查外部MCP是否启用 + if !cfg.ExternalMCPEnable && !(cfg.Enabled && !cfg.Disabled) { + return false // MCP未启用,所有工具都禁用 + } + + // MCP已启用,检查单个工具的启用状态 + // 如果ToolEnabled为空或未设置该工具,默认为启用(向后兼容) + if cfg.ToolEnabled == nil { + // 未设置工具状态,默认为启用 + } else if toolEnabled, exists := cfg.ToolEnabled[toolName]; exists { + // 使用配置的工具状态 + if !toolEnabled { + return false + } + } + // 工具未在配置中,默认为启用 + + // 最后检查外部MCP是否已连接 + client, exists := h.externalMCPMgr.GetClient(mcpName) + if !exists || !client.IsConnected() { + return false // 未连接时视为禁用 + } + + return true +} + +// formatToolDescription 格式化工具描述(限制长度) +func (h *ConfigHandler) formatToolDescription(shortDesc, fullDesc string) string { + description := shortDesc + if description == "" { + description = fullDesc + } + if len(description) > 100 { + description = description[:100] + "..." + } + return description +} diff --git a/internal/mcp/external_manager.go b/internal/mcp/external_manager.go index b86aca0d..1d9c3164 100644 --- a/internal/mcp/external_manager.go +++ b/internal/mcp/external_manager.go @@ -25,6 +25,8 @@ type ExternalMCPManager struct { errors map[string]string // 错误信息 toolCounts map[string]int // 工具数量缓存 toolCountsMu sync.RWMutex // 工具数量缓存的锁 + toolCache map[string][]Tool // 工具列表缓存:MCP名称 -> 工具列表 + toolCacheMu sync.RWMutex // 工具列表缓存的锁 stopRefresh chan struct{} // 停止后台刷新的信号 refreshWg sync.WaitGroup // 等待后台刷新goroutine完成 mu sync.RWMutex @@ -46,6 +48,7 @@ func NewExternalMCPManagerWithStorage(logger *zap.Logger, storage MonitorStorage stats: make(map[string]*ToolStats), errors: make(map[string]string), toolCounts: make(map[string]int), + toolCache: make(map[string][]Tool), stopRefresh: make(chan struct{}), } // 启动后台刷新工具数量的goroutine @@ -119,6 +122,11 @@ func (m *ExternalMCPManager) RemoveConfig(name string) error { delete(m.toolCounts, name) m.toolCountsMu.Unlock() + // 清理工具列表缓存 + m.toolCacheMu.Lock() + delete(m.toolCache, name) + m.toolCacheMu.Unlock() + return nil } @@ -196,12 +204,14 @@ func (m *ExternalMCPManager) StartClient(name string) error { m.mu.Lock() delete(m.errors, name) m.mu.Unlock() - // 立即刷新工具数量(HTTP/stdio 等可马上拿到) + // 立即刷新工具数量和工具列表缓存 m.triggerToolCountRefresh() + m.refreshToolCache(name, client) // 2 秒后再刷新一次,覆盖 SSE/Streamable 等需稍等就绪的远端 go func() { time.Sleep(2 * time.Second) m.triggerToolCountRefresh() + m.refreshToolCache(name, client) }() } }() @@ -258,6 +268,11 @@ func (m *ExternalMCPManager) GetError(name string) string { } // GetAllTools 获取所有外部MCP的工具 +// 优先从已连接的客户端获取,如果连接断开则返回缓存的工具列表 +// 策略: +// - error 状态:不使用缓存,直接跳过(配置错误或服务不可用) +// - disconnected/connecting 状态:使用缓存(临时断开) +// - connected 状态:正常获取,失败时降级使用缓存 func (m *ExternalMCPManager) GetAllTools(ctx context.Context) ([]Tool, error) { m.mu.RLock() clients := make(map[string]ExternalMCPClient) @@ -267,17 +282,21 @@ func (m *ExternalMCPManager) GetAllTools(ctx context.Context) ([]Tool, error) { m.mu.RUnlock() var allTools []Tool - for name, client := range clients { - if !client.IsConnected() { - continue - } + var hasError bool + var lastError error - tools, err := client.ListTools(ctx) + // 使用较短的超时时间进行快速检查(3秒),避免阻塞 + quickCtx, quickCancel := context.WithTimeout(ctx, 3*time.Second) + defer quickCancel() + + for name, client := range clients { + tools, err := m.getToolsForClient(name, client, quickCtx) if err != nil { - m.logger.Warn("获取外部MCP工具列表失败", - zap.String("name", name), - zap.Error(err), - ) + // 记录错误,但继续处理其他客户端 + hasError = true + if lastError == nil { + lastError = err + } continue } @@ -288,9 +307,97 @@ func (m *ExternalMCPManager) GetAllTools(ctx context.Context) ([]Tool, error) { } } + // 如果有错误但至少返回了一些工具,不返回错误(部分成功) + if hasError && len(allTools) == 0 { + return nil, fmt.Errorf("获取外部MCP工具失败: %w", lastError) + } + return allTools, nil } +// getToolsForClient 获取指定客户端的工具列表 +// 返回工具列表和错误(如果完全无法获取) +func (m *ExternalMCPManager) getToolsForClient(name string, client ExternalMCPClient, ctx context.Context) ([]Tool, error) { + status := client.GetStatus() + + // error 状态:不使用缓存,直接返回错误 + if status == "error" { + m.logger.Debug("跳过连接失败的外部MCP(不使用缓存)", + zap.String("name", name), + zap.String("status", status), + ) + return nil, fmt.Errorf("外部MCP连接失败: %s", name) + } + + // 已连接:尝试获取最新工具列表 + if client.IsConnected() { + tools, err := client.ListTools(ctx) + if err != nil { + // 获取失败,尝试使用缓存 + return m.getCachedTools(name, "连接正常但获取失败", err) + } + + // 获取成功,更新缓存 + m.updateToolCache(name, tools) + return tools, nil + } + + // 未连接:根据状态决定是否使用缓存 + if status == "disconnected" || status == "connecting" { + return m.getCachedTools(name, fmt.Sprintf("客户端临时断开(状态: %s)", status), nil) + } + + // 其他未知状态,不使用缓存 + m.logger.Debug("跳过外部MCP(未知状态)", + zap.String("name", name), + zap.String("status", status), + ) + return nil, fmt.Errorf("外部MCP状态未知: %s (状态: %s)", name, status) +} + +// getCachedTools 获取缓存的工具列表 +func (m *ExternalMCPManager) getCachedTools(name, reason string, originalErr error) ([]Tool, error) { + m.toolCacheMu.RLock() + cachedTools, hasCache := m.toolCache[name] + m.toolCacheMu.RUnlock() + + if hasCache && len(cachedTools) > 0 { + m.logger.Debug("使用缓存的工具列表", + zap.String("name", name), + zap.String("reason", reason), + zap.Int("count", len(cachedTools)), + zap.Error(originalErr), + ) + return cachedTools, nil + } + + // 无缓存,返回错误 + if originalErr != nil { + return nil, fmt.Errorf("获取外部MCP工具失败且无缓存: %w", originalErr) + } + return nil, fmt.Errorf("外部MCP无缓存工具: %s", name) +} + +// updateToolCache 更新工具列表缓存 +func (m *ExternalMCPManager) updateToolCache(name string, tools []Tool) { + m.toolCacheMu.Lock() + m.toolCache[name] = tools + m.toolCacheMu.Unlock() + + // 如果返回空列表,记录警告 + if len(tools) == 0 { + m.logger.Warn("外部MCP返回空工具列表", + zap.String("name", name), + zap.String("hint", "服务可能暂时不可用,工具列表为空"), + ) + } else { + m.logger.Debug("工具列表缓存已更新", + zap.String("name", name), + zap.Int("count", len(tools)), + ) + } +} + // CallTool 调用外部MCP工具(返回执行ID) func (m *ExternalMCPManager) CallTool(ctx context.Context, toolName string, args map[string]interface{}) (*ToolResult, string, error) { // 解析工具名称:name::toolName @@ -307,8 +414,18 @@ func (m *ExternalMCPManager) CallTool(ctx context.Context, toolName string, args return nil, "", fmt.Errorf("外部MCP客户端不存在: %s", mcpName) } + // 检查连接状态,如果未连接或状态为error,不允许调用 if !client.IsConnected() { - return nil, "", fmt.Errorf("外部MCP客户端未连接: %s", mcpName) + status := client.GetStatus() + if status == "error" { + // 获取错误信息(如果有) + errorMsg := m.GetError(mcpName) + if errorMsg != "" { + return nil, "", fmt.Errorf("外部MCP连接失败: %s (错误: %s)", mcpName, errorMsg) + } + return nil, "", fmt.Errorf("外部MCP连接失败: %s", mcpName) + } + return nil, "", fmt.Errorf("外部MCP客户端未连接: %s (状态: %s)", mcpName, status) } // 创建执行记录 @@ -694,6 +811,40 @@ func (m *ExternalMCPManager) refreshToolCounts() { m.toolCountsMu.Unlock() } +// refreshToolCache 刷新指定MCP的工具列表缓存 +func (m *ExternalMCPManager) refreshToolCache(name string, client ExternalMCPClient) { + if !client.IsConnected() { + return + } + + // 检查状态,如果是error状态,不更新缓存 + status := client.GetStatus() + if status == "error" { + m.logger.Debug("跳过刷新工具列表缓存(连接失败)", + zap.String("name", name), + zap.String("status", status), + ) + return + } + + // 使用较短的超时时间(5秒) + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + tools, err := client.ListTools(ctx) + if err != nil { + m.logger.Debug("刷新工具列表缓存失败", + zap.String("name", name), + zap.Error(err), + ) + // 刷新失败时不更新缓存,保留旧缓存(如果有) + return + } + + // 使用统一的缓存更新方法 + m.updateToolCache(name, tools) +} + // startToolCountRefresh 启动后台刷新工具数量的goroutine func (m *ExternalMCPManager) startToolCountRefresh() { m.refreshWg.Add(1) @@ -826,8 +977,13 @@ func (m *ExternalMCPManager) connectClient(name string, serverCfg config.Externa zap.String("name", name), ) - // 连接成功,触发工具数量刷新 + // 连接成功,触发工具数量刷新和工具列表缓存刷新 m.triggerToolCountRefresh() + m.mu.RLock() + if client, exists := m.clients[name]; exists { + m.refreshToolCache(name, client) + } + m.mu.RUnlock() return nil } @@ -933,6 +1089,11 @@ func (m *ExternalMCPManager) StopAll() { m.toolCounts = make(map[string]int) m.toolCountsMu.Unlock() + // 清理所有工具列表缓存 + m.toolCacheMu.Lock() + m.toolCache = make(map[string][]Tool) + m.toolCacheMu.Unlock() + // 停止后台刷新(使用 select 避免重复关闭 channel) select { case <-m.stopRefresh: diff --git a/web/static/js/router.js b/web/static/js/router.js index 3068ff0f..66d4e50d 100644 --- a/web/static/js/router.js +++ b/web/static/js/router.js @@ -254,16 +254,25 @@ function initPage(pageId) { break; case 'mcp-management': // 初始化MCP管理 + // 先加载外部MCP列表(快速),然后加载工具列表 if (typeof loadExternalMCPs === 'function') { - loadExternalMCPs(); + loadExternalMCPs().catch(err => { + console.warn('加载外部MCP列表失败:', err); + }); } // 加载工具列表(MCP工具配置已移到MCP管理页面) + // 使用异步加载,避免阻塞页面渲染 if (typeof loadToolsList === 'function') { // 确保工具分页设置已初始化 if (typeof getToolsPageSize === 'function' && typeof toolsPagination !== 'undefined') { toolsPagination.pageSize = getToolsPageSize(); } - loadToolsList(1, ''); + // 延迟加载,让页面先渲染 + setTimeout(() => { + loadToolsList(1, '').catch(err => { + console.error('加载工具列表失败:', err); + }); + }, 100); } break; case 'vulnerabilities': diff --git a/web/static/js/settings.js b/web/static/js/settings.js index e5273ab4..a06d1e08 100644 --- a/web/static/js/settings.js +++ b/web/static/js/settings.js @@ -183,6 +183,14 @@ let toolsSearchKeyword = ''; // 加载工具列表(分页) async function loadToolsList(page = 1, searchKeyword = '') { + const toolsList = document.getElementById('tools-list'); + + // 显示加载状态 + if (toolsList) { + // 清空整个容器,包括可能存在的分页控件 + toolsList.innerHTML = '
⏳ 正在加载工具列表...
'; + } + try { // 在加载新页面之前,先保存当前页的状态到全局映射 saveCurrentPageToolStates(); @@ -193,7 +201,15 @@ async function loadToolsList(page = 1, searchKeyword = '') { url += `&search=${encodeURIComponent(searchKeyword)}`; } - const response = await apiFetch(url); + // 使用较短的超时时间(10秒),避免长时间等待 + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), 10000); + + const response = await apiFetch(url, { + signal: controller.signal + }); + clearTimeout(timeoutId); + if (!response.ok) { throw new Error('获取工具列表失败'); } @@ -224,9 +240,12 @@ async function loadToolsList(page = 1, searchKeyword = '') { renderToolsPagination(); } catch (error) { console.error('加载工具列表失败:', error); - const toolsList = document.getElementById('tools-list'); if (toolsList) { - toolsList.innerHTML = `
加载工具列表失败: ${escapeHtml(error.message)}
`; + const isTimeout = error.name === 'AbortError' || error.message.includes('timeout'); + const errorMsg = isTimeout + ? '加载工具列表超时,可能是外部MCP连接较慢。请点击"刷新"按钮重试,或检查外部MCP连接状态。' + : `加载工具列表失败: ${escapeHtml(error.message)}`; + toolsList.innerHTML = `
${errorMsg}
`; } } } @@ -281,9 +300,21 @@ function renderToolsList() { const toolsList = document.getElementById('tools-list'); if (!toolsList) return; - // 只渲染列表部分,分页控件单独渲染 - const listContainer = toolsList.querySelector('.tools-list-items') || document.createElement('div'); - listContainer.className = 'tools-list-items'; + // 移除可能存在的分页控件(会在 renderToolsPagination 中重新添加) + const oldPagination = toolsList.querySelector('.tools-pagination'); + if (oldPagination) { + oldPagination.remove(); + } + + // 获取或创建列表容器 + let listContainer = toolsList.querySelector('.tools-list-items'); + if (!listContainer) { + listContainer = document.createElement('div'); + listContainer.className = 'tools-list-items'; + toolsList.appendChild(listContainer); + } + + // 清空列表容器内容(移除加载提示) listContainer.innerHTML = ''; if (allTools.length === 0) {