diff --git a/src/main/ipc.ts b/src/main/ipc.ts index 9a77cd7..0d98427 100644 --- a/src/main/ipc.ts +++ b/src/main/ipc.ts @@ -48,6 +48,7 @@ import { handleCompress } from './tool-handlers.js'; import { browserOpen, browserScreenshot, browserEvaluate, browserExtract, browserClick, browserType, browserScroll, browserClose } from './browser.js'; +import { startServer, stopServer, stopAllServers, callTool, getAllTools, getServerStatuses, refreshTools } from './mcp-manager.js'; import { getAllowedDirs, getBlockedDirs, setAllowedDirs } from './tool-security.js'; import { startProcess, killProcess, getWorkspaceDir, setWorkspaceDir, listWorkspaceDir } from './workspace.js'; @@ -408,4 +409,29 @@ export async function setupIPC(): Promise { try { incrementSkillUsage(id, success, durationMs); return { success: true }; } catch (err) { return { success: false, error: (err as Error).message }; } }); + + // ── MCP IPC ── + ipcMain.handle('mcp:startServer', async (_, config) => { + return startServer(config); + }); + ipcMain.handle('mcp:stopServer', (_, name: string) => { + stopServer(name); + return { success: true }; + }); + ipcMain.handle('mcp:stopAll', () => { + stopAllServers(); + return { success: true }; + }); + ipcMain.handle('mcp:callTool', async (_, serverName: string, toolName: string, args: Record) => { + return callTool(serverName, toolName, args); + }); + ipcMain.handle('mcp:getTools', () => { + return getAllTools(); + }); + ipcMain.handle('mcp:getStatuses', () => { + return getServerStatuses(); + }); + ipcMain.handle('mcp:refreshTools', async (_, name: string) => { + return refreshTools(name); + }); } diff --git a/src/main/main.ts b/src/main/main.ts index 17febba..ac2b96b 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -11,6 +11,7 @@ import { createMenu } from './menu.js'; import { showNotification } from './utils.js'; import { ensureWorkspaceDir, killAllProcesses } from './workspace.js'; import { browserClose } from './browser.js'; +import { stopAllServers } from './mcp-manager.js'; // ── 全局错误处理:写入文件 + 弹窗提示 ── const ERROR_LOG = path.join(app.getPath('userData'), 'startup-error.log'); @@ -197,6 +198,8 @@ app.on('before-quit', () => { isQuitting = true; // 清理浏览器 browserClose(); + // 清理 MCP 服务器 + stopAllServers(); // 清理所有工作空间进程 killAllProcesses(); // 通知渲染进程释放显存 diff --git a/src/main/mcp-manager.ts b/src/main/mcp-manager.ts new file mode 100644 index 0000000..7e51edc --- /dev/null +++ b/src/main/mcp-manager.ts @@ -0,0 +1,296 @@ +/** + * MCP Manager - Model Context Protocol 主进程管理器 + * 通过 stdio JSON-RPC 2.0 与 MCP Server 通信 + */ + +import { spawn, ChildProcess } from 'child_process'; +import * as path from 'path'; +import * as fs from 'fs'; +import { mainWindow } from './main.js'; + +function sendLog(level: 'info' | 'success' | 'warn' | 'error', message: string, detail?: string): void { + mainWindow?.webContents.send('main:log', { level, message, detail }); +} + +// ─── 类型定义 ─── + +interface JSONRPCRequest { + jsonrpc: '2.0'; + id: number; + method: string; + params?: Record; +} + +interface JSONRPCResponse { + jsonrpc: '2.0'; + id: number; + result?: unknown; + error?: { code: number; message: string; data?: unknown }; +} + +interface MCPTool { + name: string; + description?: string; + inputSchema: { + type: string; + properties?: Record; + required?: string[]; + }; +} + +interface MCPServerInstance { + config: { name: string; command: string; args: string[]; enabled: boolean; description?: string }; + process: ChildProcess; + requestId: number; + pendingRequests: Map void; reject: (reason: Error) => void; timer: ReturnType }>; + buffer: string; + tools: MCPTool[]; + initialized: boolean; + capabilities: Record; +} + +// ─── 状态 ─── + +const servers = new Map(); +const MCP_TIMEOUT = 30000; // 30 秒超时 + +// ─── JSON-RPC 通信 ─── + +function sendRequest(server: MCPServerInstance, method: string, params?: Record): Promise { + return new Promise((resolve, reject) => { + const id = ++server.requestId; + const request: JSONRPCRequest = { jsonrpc: '2.0', id, method, params }; + + const timer = setTimeout(() => { + server.pendingRequests.delete(id); + reject(new Error(`MCP 请求超时: ${server.config.name} / ${method} (${MCP_TIMEOUT}ms)`)); + }, MCP_TIMEOUT); + + server.pendingRequests.set(id, { resolve, reject, timer }); + + const line = JSON.stringify(request) + '\n'; + try { + server.process.stdin!.write(line); + } catch (err) { + server.pendingRequests.delete(id); + clearTimeout(timer); + reject(new Error(`MCP 写入失败: ${(err as Error).message}`)); + } + }); +} + +function handleResponse(server: MCPServerInstance, response: JSONRPCResponse): void { + const pending = server.pendingRequests.get(response.id); + if (!pending) return; + + clearTimeout(pending.timer); + server.pendingRequests.delete(response.id); + + if (response.error) { + pending.reject(new Error(`MCP 错误 [${response.error.code}]: ${response.error.message}`)); + } else { + pending.resolve(response.result); + } +} + +function processData(server: MCPServerInstance, data: Buffer): void { + server.buffer += data.toString('utf-8'); + const lines = server.buffer.split('\n'); + server.buffer = lines.pop() || ''; + + for (const line of lines) { + if (!line.trim()) continue; + try { + const msg = JSON.parse(line); + if (msg.id !== undefined && (msg.result !== undefined || msg.error !== undefined)) { + handleResponse(server, msg as JSONRPCResponse); + } + // 忽略通知(无 id 的消息) + } catch { + // 非 JSON 行,忽略(有些 server 会输出启动日志) + } + } +} + +// ─── 服务器生命周期 ─── + +export async function startServer(config: { name: string; command: string; args: string[]; enabled: boolean; description?: string }): Promise<{ success: boolean; error?: string }> { + // 已在运行则先停止 + if (servers.has(config.name)) { + stopServer(config.name); + } + + sendLog('info', `🔌 MCP 启动: ${config.name}`, `${config.command} ${config.args.join(' ')}`); + + try { + const proc = spawn(config.command, config.args, { + stdio: ['pipe', 'pipe', 'pipe'], + env: { ...process.env }, + cwd: process.cwd() + }); + + const server: MCPServerInstance = { + config, + process: proc, + requestId: 0, + pendingRequests: new Map(), + buffer: '', + tools: [], + initialized: false, + capabilities: {} + }; + + servers.set(config.name, server); + + proc.stdout.on('data', (data: Buffer) => processData(server, data)); + + proc.stderr.on('data', (data: Buffer) => { + const msg = data.toString('utf-8').trim(); + if (msg) sendLog('info', `🔌 MCP [${config.name}] stderr`, msg.slice(0, 200)); + }); + + proc.on('close', (code) => { + sendLog(code === 0 ? 'info' : 'warn', `🔌 MCP 关闭: ${config.name}`, `exit code: ${code}`); + cleanupServer(server); + servers.delete(config.name); + }); + + proc.on('error', (err) => { + sendLog('error', `🔌 MCP 启动失败: ${config.name}`, err.message); + cleanupServer(server); + servers.delete(config.name); + }); + + // MCP initialize 握手 + try { + const initResult = await sendRequest(server, 'initialize', { + protocolVersion: '2024-11-05', + capabilities: { tools: {} }, + clientInfo: { name: 'Metona Ollama', version: '5.0.0' } + }) as Record; + + server.capabilities = (initResult.capabilities as Record) || {}; + server.initialized = true; + + // 发送 initialized 通知 + const notification = JSON.stringify({ jsonrpc: '2.0', method: 'notifications/initialized' }) + '\n'; + server.process.stdin!.write(notification); + + sendLog('success', `🔌 MCP 已连接: ${config.name}`, `协议 ${(initResult.protocolVersion as string) || 'unknown'}`); + + // 获取工具列表 + await refreshTools(config.name); + + return { success: true }; + } catch (err) { + sendLog('error', `🔌 MCP 握手失败: ${config.name}`, (err as Error).message); + stopServer(config.name); + return { success: false, error: (err as Error).message }; + } + } catch (err) { + sendLog('error', `🔌 MCP spawn 失败: ${config.name}`, (err as Error).message); + return { success: false, error: (err as Error).message }; + } +} + +function cleanupServer(server: MCPServerInstance): void { + for (const [, pending] of server.pendingRequests) { + clearTimeout(pending.timer); + pending.reject(new Error('MCP 服务器已关闭')); + } + server.pendingRequests.clear(); + try { server.process.kill('SIGTERM'); } catch { /* ignore */ } +} + +export function stopServer(name: string): void { + const server = servers.get(name); + if (!server) return; + cleanupServer(server); + servers.delete(name); + sendLog('info', `🔌 MCP 已停止: ${name}`); +} + +export function stopAllServers(): void { + for (const [name] of servers) { + stopServer(name); + } +} + +// ─── 工具操作 ─── + +export async function refreshTools(name: string): Promise { + const server = servers.get(name); + if (!server || !server.initialized) return []; + + try { + const result = await sendRequest(server, 'tools/list') as { tools?: MCPTool[] }; + server.tools = result.tools || []; + sendLog('success', `🔌 MCP [${name}] ${server.tools.length} 个工具`, + server.tools.map(t => t.name).join(', ')); + return server.tools; + } catch (err) { + sendLog('error', `🔌 MCP [${name}] 获取工具失败`, (err as Error).message); + return []; + } +} + +export async function callTool(serverName: string, toolName: string, args: Record): Promise<{ success: boolean; result?: unknown; error?: string }> { + const server = servers.get(serverName); + if (!server || !server.initialized) { + return { success: false, error: `MCP 服务器 ${serverName} 未运行` }; + } + + try { + const result = await sendRequest(server, 'tools/call', { name: toolName, arguments: args }); + return { success: true, result }; + } catch (err) { + return { success: false, error: (err as Error).message }; + } +} + +// ─── 查询 ─── + +export interface MCPToolInfo { + serverName: string; + name: string; + fullName: string; + description: string; + inputSchema: MCPTool['inputSchema']; +} + +/** 获取所有已连接服务器的工具列表 */ +export function getAllTools(): MCPToolInfo[] { + const tools: MCPToolInfo[] = []; + for (const [serverName, server] of servers) { + if (!server.initialized) continue; + for (const tool of server.tools) { + tools.push({ + serverName, + name: tool.name, + fullName: `mcp_${serverName}_${tool.name}`, + description: `[MCP:${serverName}] ${tool.description || ''}`, + inputSchema: tool.inputSchema + }); + } + } + return tools; +} + +/** 获取服务器状态 */ +export function getServerStatuses(): Array<{ name: string; running: boolean; toolCount: number; error?: string }> { + const statuses: Array<{ name: string; running: boolean; toolCount: number; error?: string }> = []; + for (const [name, server] of servers) { + statuses.push({ + name, + running: server.initialized, + toolCount: server.tools.length + }); + } + return statuses; +} + +/** 检查服务器是否在运行 */ +export function isServerRunning(name: string): boolean { + const server = servers.get(name); + return !!server && server.initialized; +} diff --git a/src/main/preload.ts b/src/main/preload.ts index 6f2c5dd..1bf739b 100644 --- a/src/main/preload.ts +++ b/src/main/preload.ts @@ -103,5 +103,14 @@ contextBridge.exposeInMainWorld('metonaDesktop', { }, /** 终止当前 AI 命令 */ cmdKill: () => ipcRenderer.invoke('cmd:kill') + }, + mcp: { + startServer: (config: { name: string; command: string; args: string[]; enabled: boolean; description?: string }) => ipcRenderer.invoke('mcp:startServer', config), + stopServer: (name: string) => ipcRenderer.invoke('mcp:stopServer', name), + stopAll: () => ipcRenderer.invoke('mcp:stopAll'), + callTool: (serverName: string, toolName: string, args: Record) => ipcRenderer.invoke('mcp:callTool', serverName, toolName, args), + getTools: () => ipcRenderer.invoke('mcp:getTools'), + getStatuses: () => ipcRenderer.invoke('mcp:getStatuses'), + refreshTools: (name: string) => ipcRenderer.invoke('mcp:refreshTools', name) } }); diff --git a/src/renderer/components/settings-modal.ts b/src/renderer/components/settings-modal.ts index 90a7d97..881a6ee 100644 --- a/src/renderer/components/settings-modal.ts +++ b/src/renderer/components/settings-modal.ts @@ -151,6 +151,78 @@ export function initSettingsModal(): void { logSetting('检查间隔', `${ms / 60000} 分钟`); }); + // ── v5.0 MCP 服务器管理 ── + async function renderMCPServerList(): Promise { + const { getMCPServers, getMCPServerStatuses, toggleMCPServer, removeMCPServer } = await import('../services/mcp-client.js'); + const { refreshMCPTools } = await import('../services/tool-registry.js'); + const container = document.querySelector('#mcpServerList')!; + const servers = await getMCPServers(); + const statuses = await getMCPServerStatuses(); + + if (servers.length === 0) { + container.innerHTML = '

未配置 MCP 服务器。MCP 允许连接外部工具服务。

'; + return; + } + + container.innerHTML = servers.map(s => { + const status = statuses.find(st => st.name === s.name); + const running = status?.running || false; + const toolCount = status?.toolCount || 0; + return `
+ + ${s.name} + ${s.command} ${s.args.join(' ')}${running ? ` (${toolCount} 工具)` : ''} + + +
`; + }).join(''); + + container.querySelectorAll('.mcp-toggle').forEach(btn => { + btn.addEventListener('click', async () => { + const name = (btn as HTMLElement).dataset.name!; + await toggleMCPServer(name); + await refreshMCPTools(); + await renderMCPServerList(); + }); + }); + + container.querySelectorAll('.mcp-delete').forEach(btn => { + btn.addEventListener('click', async () => { + const name = (btn as HTMLElement).dataset.name!; + await removeMCPServer(name); + await refreshMCPTools(); + await renderMCPServerList(); + }); + }); + } + + document.querySelector('#btnAddMCPServer')!.addEventListener('click', async () => { + const { addMCPServer } = await import('../services/mcp-client.js'); + const { refreshMCPTools } = await import('../services/tool-registry.js'); + const name = (document.querySelector('#inputMCPName') as HTMLInputElement).value.trim(); + const command = (document.querySelector('#inputMCPCommand') as HTMLInputElement).value.trim(); + const argsStr = (document.querySelector('#inputMCPArgs') as HTMLInputElement).value.trim(); + + if (!name || !command) { + showToast('请填写名称和命令', 'warning'); + return; + } + + const args = argsStr ? argsStr.split(/\s+/) : []; + await addMCPServer({ name, command, args, enabled: true }); + await refreshMCPTools(); + (document.querySelector('#inputMCPName') as HTMLInputElement).value = ''; + (document.querySelector('#inputMCPCommand') as HTMLInputElement).value = ''; + (document.querySelector('#inputMCPArgs') as HTMLInputElement).value = ''; + showToast(`MCP 服务器已添加: ${name}`, 'success'); + await renderMCPServerList(); + }); + + // 设置面板打开时刷新 MCP 列表 + document.querySelector('#btnSettings')!.addEventListener('click', () => { + setTimeout(renderMCPServerList, 100); + }); + // ── v5.0 Cron 定时任务 ── document.querySelector('#btnAddCronTask')!.addEventListener('click', async () => { const name = (document.querySelector('#inputCronName') as HTMLInputElement).value.trim(); diff --git a/src/renderer/index.html b/src/renderer/index.html index 5f08a39..a6dca93 100644 --- a/src/renderer/index.html +++ b/src/renderer/index.html @@ -347,6 +347,16 @@ +
+ +
+ + + + +
+
+
diff --git a/src/renderer/main.ts b/src/renderer/main.ts index fa8f2fb..3fb7781 100644 --- a/src/renderer/main.ts +++ b/src/renderer/main.ts @@ -25,6 +25,7 @@ import { initMemoryManager } from './services/memory-manager.js'; import { restoreHeartbeat } from './components/settings-modal.js'; import { restoreCronTasks } from './services/cron-manager.js'; import { setToolEnabled, setRunCommandMode, refreshMCPTools } from './services/tool-registry.js'; +import { startAllMCPServers, stopAllMCPServers } from './services/mcp-client.js'; import { initToolConfirmModal } from './components/tool-confirm-modal.js'; import { initWorkspacePanel } from './components/workspace-panel.js'; import { initLogPanel, addLog } from './services/log-service.js'; @@ -362,6 +363,7 @@ async function init(): Promise { if (toggleTC) toggleTC.checked = toolCallingEnabled; // ── MCP 工具注册 ── + await startAllMCPServers(); await refreshMCPTools(); // ── Agent 记忆设置 ── diff --git a/src/renderer/services/mcp-client.ts b/src/renderer/services/mcp-client.ts index 8d98c66..ca9e3b4 100644 --- a/src/renderer/services/mcp-client.ts +++ b/src/renderer/services/mcp-client.ts @@ -1,17 +1,11 @@ /** - * MCP Client - Model Context Protocol 客户端 (v4.3) - * 轻量级实现:管理 MCP Server 配置,提供工具注册框架 - * - * MCP Server 配置存储在 settings.mcp_servers 中,格式: - * [ - * { "name": "filesystem", "command": "npx", "args": ["-y", "@anthropic/mcp-filesystem"], "enabled": true }, - * { "name": "sqlite", "command": "npx", "args": ["-y", "@anthropic/mcp-sqlite", "./data.db"], "enabled": false } - * ] + * MCP Client - Model Context Protocol 渲染端客户端 + * 管理 MCP Server 配置,通过 IPC 调用主进程进行协议通信 */ import { state, KEYS } from '../state/state.js'; -import { logInfo, logWarn } from './log-service.js'; -import type { ToolDefinition } from '../types.js'; +import { logInfo, logWarn, logSuccess, logError } from './log-service.js'; +import type { ToolDefinition, ToolResult } from '../types.js'; export interface MCPServerConfig { name: string; @@ -21,7 +15,8 @@ export interface MCPServerConfig { description?: string; } -/** 获取所有 MCP 服务器配置 */ +// ─── 配置管理 ─── + export async function getMCPServers(): Promise { const db = state.get(KEYS.DB); if (!db) return []; @@ -32,13 +27,11 @@ export async function getMCPServers(): Promise { } } -/** 保存 MCP 服务器配置 */ export async function saveMCPServers(servers: MCPServerConfig[]): Promise { const db = state.get(KEYS.DB); if (db) await db.saveSetting('mcp_servers', servers); } -/** 添加 MCP 服务器 */ export async function addMCPServer(config: MCPServerConfig): Promise { const servers = await getMCPServers(); const existing = servers.findIndex(s => s.name === config.name); @@ -51,14 +44,14 @@ export async function addMCPServer(config: MCPServerConfig): Promise { logInfo(`MCP Server 已${existing >= 0 ? '更新' : '添加'}: ${config.name}`); } -/** 删除 MCP 服务器 */ export async function removeMCPServer(name: string): Promise { + const bridge = (window as any).metonaDesktop; + if (bridge?.mcp) await bridge.mcp.stopServer(name); const servers = await getMCPServers(); await saveMCPServers(servers.filter(s => s.name !== name)); logInfo(`MCP Server 已删除: ${name}`); } -/** 切换 MCP 服务器启用状态 */ export async function toggleMCPServer(name: string): Promise { const servers = await getMCPServers(); const server = servers.find(s => s.name === name); @@ -69,22 +62,108 @@ export async function toggleMCPServer(name: string): Promise { return server.enabled; } -/** 获取已启用的 MCP 服务器 */ export async function getEnabledMCPServers(): Promise { const servers = await getMCPServers(); return servers.filter(s => s.enabled); } -/** - * 获取 MCP 工具定义(供 Agent 使用) - * 目前返回占位工具定义,实际 MCP 协议通信需通过主进程实现 - */ +// ─── MCP 服务器生命周期 ─── + +/** 启动所有已启用的 MCP 服务器 */ +export async function startAllMCPServers(): Promise { + const bridge = (window as any).metonaDesktop; + if (!bridge?.mcp) return; + + const configs = await getEnabledMCPServers(); + for (const config of configs) { + try { + const result = await bridge.mcp.startServer(config); + if (result.success) { + logSuccess(`MCP 服务器已启动: ${config.name}`); + } else { + logWarn(`MCP 服务器启动失败: ${config.name}`, result.error); + } + } catch (err) { + logError(`MCP 服务器启动异常: ${config.name}`, (err as Error).message); + } + } +} + +/** 停止所有 MCP 服务器 */ +export async function stopAllMCPServers(): Promise { + const bridge = (window as any).metonaDesktop; + if (bridge?.mcp) await bridge.mcp.stopAll(); +} + +/** 获取 MCP 服务器状态 */ +export async function getMCPServerStatuses(): Promise> { + const bridge = (window as any).metonaDesktop; + if (!bridge?.mcp) return []; + try { + return await bridge.mcp.getStatuses(); + } catch { + return []; + } +} + +// ─── 工具定义(供 Agent 使用)─── + +/** 从主进程获取所有 MCP 工具并转换为 ToolDefinition 格式 */ export async function getMCPToolDefinitions(): Promise { - const enabled = await getEnabledMCPServers(); - // MCP 工具通过主进程的 MCP bridge 动态注册 - // 这里返回空数组,工具定义由 MCP bridge 在 IPC 层注入 - if (enabled.length > 0) { - logInfo(`MCP: ${enabled.length} 个服务器已启用`, enabled.map(s => s.name).join(', ')); + const bridge = (window as any).metonaDesktop; + if (!bridge?.mcp) return []; + + try { + const mcpTools = await bridge.mcp.getTools() as Array<{ + serverName: string; + name: string; + fullName: string; + description: string; + inputSchema: { type: string; properties?: Record; required?: string[] }; + }>; + + if (mcpTools.length === 0) return []; + + logInfo(`MCP 工具已注册: ${mcpTools.length} 个`, mcpTools.map(t => t.fullName).join(', ')); + + return mcpTools.map(t => ({ + type: 'function' as const, + function: { + name: t.fullName, + description: t.description, + parameters: { + type: 'object' as const, + required: t.inputSchema.required || [], + properties: (t.inputSchema.properties || {}) as Record + } + } + })); + } catch (err) { + logError('MCP 工具获取失败', (err as Error).message); + return []; + } +} + +/** 执行 MCP 工具调用 */ +export async function executeMCPTool(toolName: string, args: Record): Promise { + const bridge = (window as any).metonaDesktop; + if (!bridge?.mcp) { + return { success: false, error: 'MCP API 不可用' }; + } + + // 解析工具全名: mcp_{serverName}_{toolName} + const match = toolName.match(/^mcp_(.+?)_(.+)$/); + if (!match) { + return { success: false, error: `无效的 MCP 工具名: ${toolName}` }; + } + + const serverName = match[1]; + const actualToolName = match[2]; + + try { + const result = await bridge.mcp.callTool(serverName, actualToolName, args); + return result; + } catch (err) { + return { success: false, error: (err as Error).message }; } - return []; } diff --git a/src/renderer/services/tool-registry.ts b/src/renderer/services/tool-registry.ts index f86b496..43a745d 100644 --- a/src/renderer/services/tool-registry.ts +++ b/src/renderer/services/tool-registry.ts @@ -595,7 +595,13 @@ export async function refreshMCPTools(): Promise { } export async function executeTool(toolName: string, args: Record): Promise { - if (!isToolEnabled(toolName)) { + // MCP 工具路由 + if (toolName.startsWith('mcp_')) { + const { executeMCPTool } = await import('./mcp-client.js'); + return executeMCPTool(toolName, args); + } + + if (!enabledTools.has(toolName)) { logError(`工具未启用: ${toolName}`); return { success: false, error: `工具 ${toolName} 未启用` }; } diff --git a/src/renderer/types.d.ts b/src/renderer/types.d.ts index 4b9db27..f1d7e73 100644 --- a/src/renderer/types.d.ts +++ b/src/renderer/types.d.ts @@ -240,6 +240,15 @@ export interface MetonaDesktopAPI { /** 终止当前 AI 命令 */ cmdKill: () => Promise<{ killed: boolean }>; }; + mcp: { + startServer: (config: { name: string; command: string; args: string[]; enabled: boolean; description?: string }) => Promise<{ success: boolean; error?: string }>; + stopServer: (name: string) => Promise<{ success: boolean }>; + stopAll: () => Promise<{ success: boolean }>; + callTool: (serverName: string, toolName: string, args: Record) => Promise<{ success: boolean; result?: unknown; error?: string }>; + getTools: () => Promise; required?: string[] } }>>; + getStatuses: () => Promise>; + refreshTools: (name: string) => Promise }>>; + }; } export interface AppInfo {