diff --git a/app/components/mcp-market.module.scss b/app/components/mcp-market.module.scss index 93c6b67de..a3025c03e 100644 --- a/app/components/mcp-market.module.scss +++ b/app/components/mcp-market.module.scss @@ -98,6 +98,10 @@ background-color: #ef4444; } + &.stopped { + background-color: #6b7280; + } + .error-message { margin-left: 4px; font-size: 12px; @@ -151,21 +155,11 @@ .mcp-market-actions { display: flex; - gap: 8px; + gap: 12px; align-items: flex-start; flex-shrink: 0; min-width: 180px; justify-content: flex-end; - - :global(.icon-button) { - transition: all 0.3s ease; - border: 1px solid transparent; - - &:hover { - transform: translateY(-1px); - filter: brightness(1.1); - } - } } } } @@ -213,30 +207,6 @@ color: var(--gray-300); } } - - :global(.icon-button) { - width: 32px; - height: 32px; - padding: 0; - border-radius: 6px; - background-color: transparent; - border: 1px solid var(--gray-200); - flex-shrink: 0; - display: flex; - align-items: center; - justify-content: center; - - &:hover { - background-color: var(--gray-100); - border-color: var(--gray-300); - } - - svg { - width: 16px; - height: 16px; - opacity: 0.7; - } - } } :global(.icon-button.add-path-button) { diff --git a/app/components/mcp-market.tsx b/app/components/mcp-market.tsx index fc088c03b..0e46e7766 100644 --- a/app/components/mcp-market.tsx +++ b/app/components/mcp-market.tsx @@ -17,16 +17,20 @@ import { getClientStatus, getClientTools, getMcpConfigFromFile, - removeMcpServer, restartAllClients, + pauseMcpServer, + resumeMcpServer, } from "../mcp/actions"; import { ListToolsResponse, McpConfigData, PresetServer, ServerConfig, + ServerStatusResponse, } from "../mcp/types"; import clsx from "clsx"; +import PlayIcon from "../icons/play.svg"; +import StopIcon from "../icons/pause.svg"; const presetServers = presetServersJson as PresetServer[]; @@ -47,13 +51,7 @@ export function McpMarketPage() { const [isLoading, setIsLoading] = useState(false); const [config, setConfig] = useState(); const [clientStatuses, setClientStatuses] = useState< - Record< - string, - { - status: "active" | "error" | "undefined"; - errorMsg: string | null; - } - > + Record >({}); // 检查服务器是否已添加 @@ -253,18 +251,74 @@ export function McpMarketPage() { }; // 移除服务器 - const removeServer = async (id: string) => { + // const removeServer = async (id: string) => { + // try { + // setIsLoading(true); + // const newConfig = await removeMcpServer(id); + // setConfig(newConfig); + + // // 移除状态 + // setClientStatuses((prev) => { + // const newStatuses = { ...prev }; + // delete newStatuses[id]; + // return newStatuses; + // }); + // } finally { + // setIsLoading(false); + // } + // }; + + // 暂停服务器 + const pauseServer = async (id: string) => { try { setIsLoading(true); - const newConfig = await removeMcpServer(id); + showToast("Stopping server..."); + const newConfig = await pauseMcpServer(id); setConfig(newConfig); - // 移除状态 - setClientStatuses((prev) => { - const newStatuses = { ...prev }; - delete newStatuses[id]; - return newStatuses; - }); + // 更新状态为暂停 + setClientStatuses((prev) => ({ + ...prev, + [id]: { status: "paused", errorMsg: null }, + })); + showToast("Server stopped successfully"); + } catch (error) { + showToast("Failed to stop server"); + console.error(error); + } finally { + setIsLoading(false); + } + }; + + // 恢复服务器 + const resumeServer = async (id: string) => { + try { + setIsLoading(true); + showToast("Starting server..."); + + // 尝试启动服务器 + const success = await resumeMcpServer(id); + + // 获取最新状态(这个状态是从 clientsMap 中获取的,反映真实状态) + const status = await getClientStatus(id); + setClientStatuses((prev) => ({ + ...prev, + [id]: status, + })); + + // 根据启动结果显示消息 + if (success) { + showToast("Server started successfully"); + } else { + throw new Error("Failed to start server"); + } + } catch (error) { + showToast( + error instanceof Error + ? error.message + : "Failed to start server, please check logs", + ); + console.error(error); } finally { setIsLoading(false); } @@ -332,7 +386,12 @@ export function McpMarketPage() { } else if (prop.type === "string") { const currentValue = userConfig[key as keyof typeof userConfig] || ""; return ( - +
{ + const status = checkServerStatus(clientId); + + const statusMap = { + undefined: null, // 未配置/未找到不显示 + paused: ( + + Stopped + + ), + active: Running, + error: ( + + Error + : {status.errorMsg} + + ), + }; + + return statusMap[status.status]; + }; + // 渲染服务器列表 const renderServerList = () => { return presetServers @@ -373,15 +455,18 @@ export function McpMarketPage() { const bStatus = checkServerStatus(b.id).status; // 定义状态优先级 - const statusPriority = { - error: 0, - active: 1, - undefined: 2, + const statusPriority: Record = { + error: 0, // 最高优先级 + active: 1, // 运行中 + paused: 2, // 已暂停 + undefined: 3, // 未配置/未找到 }; // 首先按状态排序 if (aStatus !== bStatus) { - return statusPriority[aStatus] - statusPriority[bStatus]; + return ( + (statusPriority[aStatus] || 3) - (statusPriority[bStatus] || 3) + ); } // 然后按名称排序 @@ -398,25 +483,7 @@ export function McpMarketPage() {
{server.name} - {checkServerStatus(server.id).status !== "undefined" && ( - - {checkServerStatus(server.id).status === "error" ? ( - <> - Error - - : {checkServerStatus(server.id).errorMsg} - - - ) : ( - "Active" - )} - - )} + {getServerStatusDisplay(server.id)} {server.repo && ( } text="Configure" - className={clsx({ - [styles["action-error"]]: - checkServerStatus(server.id).status === "error", - })} onClick={() => setEditingServerId(server.id)} disabled={isLoading} /> )} - } - text="Tools" - onClick={async () => { - setViewingServerId(server.id); - await loadTools(server.id); - }} - disabled={ - isLoading || - checkServerStatus(server.id).status === "error" - } - /> - } - text="Remove" - className={styles["action-danger"]} - onClick={() => removeServer(server.id)} - disabled={isLoading} - /> + {checkServerStatus(server.id).status === "paused" ? ( + <> + } + text="Start" + onClick={() => resumeServer(server.id)} + disabled={isLoading} + /> + {/* } + text="Remove" + onClick={() => removeServer(server.id)} + disabled={isLoading} + /> */} + + ) : ( + <> + } + text="Tools" + onClick={async () => { + setViewingServerId(server.id); + await loadTools(server.id); + }} + disabled={ + isLoading || + checkServerStatus(server.id).status === "error" + } + /> + } + text="Stop" + onClick={() => pauseServer(server.id)} + disabled={isLoading} + /> + + )} ) : ( } text="Add" - className={styles["action-primary"]} onClick={() => addServer(server)} disabled={isLoading} /> diff --git a/app/icons/pause.svg b/app/icons/pause.svg index 4e81ef067..08a6572d6 100644 --- a/app/icons/pause.svg +++ b/app/icons/pause.svg @@ -1 +1,3 @@ - \ No newline at end of file + + + \ No newline at end of file diff --git a/app/icons/play.svg b/app/icons/play.svg new file mode 100644 index 000000000..4a2515c6f --- /dev/null +++ b/app/icons/play.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/app/mcp/actions.ts b/app/mcp/actions.ts index c6b9fd75f..ba1525be7 100644 --- a/app/mcp/actions.ts +++ b/app/mcp/actions.ts @@ -12,6 +12,7 @@ import { McpConfigData, McpRequestMessage, ServerConfig, + ServerStatusResponse, } from "./types"; import fs from "fs/promises"; import path from "path"; @@ -22,14 +23,40 @@ const CONFIG_PATH = path.join(process.cwd(), "app/mcp/mcp_config.json"); const clientsMap = new Map(); // 获取客户端状态 -export async function getClientStatus(clientId: string) { +export async function getClientStatus( + clientId: string, +): Promise { const status = clientsMap.get(clientId); - if (!status) return { status: "undefined" as const, errorMsg: null }; + const config = await getMcpConfigFromFile(); + const serverConfig = config.mcpServers[clientId]; - return { - status: status.errorMsg ? ("error" as const) : ("active" as const), - errorMsg: status.errorMsg, - }; + // 如果配置中不存在该服务器 + if (!serverConfig) { + return { status: "undefined", errorMsg: null }; + } + + // 如果服务器配置为暂停状态 + if (serverConfig.status === "paused") { + return { status: "paused", errorMsg: null }; + } + + // 如果 clientsMap 中没有记录 + if (!status) { + return { status: "undefined", errorMsg: null }; + } + + // 如果有错误 + if (status.errorMsg) { + return { status: "error", errorMsg: status.errorMsg }; + } + + // 如果客户端正常运行 + if (status.client) { + return { status: "active", errorMsg: null }; + } + + // 如果客户端不存在 + return { status: "error", errorMsg: "Client not found" }; } // 获取客户端工具 @@ -61,6 +88,12 @@ async function initializeSingleClient( clientId: string, serverConfig: ServerConfig, ) { + // 如果服务器状态是暂停,则不初始化 + if (serverConfig.status === "paused") { + logger.info(`Skipping initialization for paused client [${clientId}]`); + return; + } + logger.info(`Initializing client [${clientId}]...`); try { const client = await createClient(clientId, serverConfig); @@ -114,6 +147,100 @@ export async function addMcpServer(clientId: string, config: ServerConfig) { } } +// 暂停服务器 +export async function pauseMcpServer(clientId: string) { + try { + const currentConfig = await getMcpConfigFromFile(); + const serverConfig = currentConfig.mcpServers[clientId]; + if (!serverConfig) { + throw new Error(`Server ${clientId} not found`); + } + + // 先更新配置 + const newConfig: McpConfigData = { + ...currentConfig, + mcpServers: { + ...currentConfig.mcpServers, + [clientId]: { + ...serverConfig, + status: "paused" as const, + }, + }, + }; + await updateMcpConfig(newConfig); + + // 然后关闭客户端 + const client = clientsMap.get(clientId); + if (client?.client) { + await removeClient(client.client); + } + clientsMap.delete(clientId); + + return newConfig; + } catch (error) { + logger.error(`Failed to pause server [${clientId}]: ${error}`); + throw error; + } +} + +// 恢复服务器 +export async function resumeMcpServer(clientId: string): Promise { + try { + const currentConfig = await getMcpConfigFromFile(); + const serverConfig = currentConfig.mcpServers[clientId]; + if (!serverConfig) { + throw new Error(`Server ${clientId} not found`); + } + + // 先尝试初始化客户端 + logger.info(`Trying to initialize client [${clientId}]...`); + try { + const client = await createClient(clientId, serverConfig); + const tools = await listTools(client); + clientsMap.set(clientId, { client, tools, errorMsg: null }); + logger.success(`Client [${clientId}] initialized successfully`); + + // 初始化成功后更新配置 + const newConfig: McpConfigData = { + ...currentConfig, + mcpServers: { + ...currentConfig.mcpServers, + [clientId]: { + ...serverConfig, + status: "active" as const, + }, + }, + }; + await updateMcpConfig(newConfig); + + // 再次确认状态 + const status = await getClientStatus(clientId); + return status.status === "active"; + } catch (error) { + const currentConfig = await getMcpConfigFromFile(); + const serverConfig = currentConfig.mcpServers[clientId]; + + // 如果配置中存在该服务器,则更新其状态为 error + if (serverConfig) { + serverConfig.status = "error"; + await updateMcpConfig(currentConfig); + } + + // 初始化失败 + clientsMap.set(clientId, { + client: null, + tools: null, + errorMsg: error instanceof Error ? error.message : String(error), + }); + logger.error(`Failed to initialize client [${clientId}]: ${error}`); + return false; + } + } catch (error) { + logger.error(`Failed to resume server [${clientId}]: ${error}`); + throw error; + } +} + // 移除服务器 export async function removeMcpServer(clientId: string) { try { diff --git a/app/mcp/preset-server.json b/app/mcp/preset-server.json index b44b841d2..84fe234bd 100644 --- a/app/mcp/preset-server.json +++ b/app/mcp/preset-server.json @@ -72,31 +72,6 @@ "baseArgs": ["-y", "@executeautomation/playwright-mcp-server"], "configurable": false }, - { - "id": "mongodb", - "name": "MongoDB", - "description": "Direct interaction with MongoDB databases", - "repo": "", - "tags": ["database", "mongodb", "nosql"], - "command": "node", - "baseArgs": ["dist/index.js"], - "configurable": true, - "configSchema": { - "properties": { - "connectionString": { - "type": "string", - "description": "MongoDB connection string", - "required": true - } - } - }, - "argsMapping": { - "connectionString": { - "type": "single", - "position": 1 - } - } - }, { "id": "difyworkflow", "name": "Dify Workflow", diff --git a/app/mcp/types.ts b/app/mcp/types.ts index da6731d28..85e94f3b8 100644 --- a/app/mcp/types.ts +++ b/app/mcp/types.ts @@ -87,11 +87,20 @@ interface McpErrorClient { errorMsg: string; } +// 服务器状态类型 +export type ServerStatus = "undefined" | "active" | "paused" | "error"; + +export interface ServerStatusResponse { + status: ServerStatus; + errorMsg: string | null; +} + // MCP 服务器配置相关类型 export interface ServerConfig { command: string; args: string[]; env?: Record; + status?: "active" | "paused" | "error"; } export interface McpConfigData {